From b37e2d9343a2551e6ba34276bfe77b8c0480911c Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Fri, 10 Oct 2025 16:47:22 -0400 Subject: [PATCH] [commands] Add Commands v3 framework (#6518) The framework fundamentally relies on the continuation API added in Java 21 (which is currently internal to the JDK). Continuations allow for call stacks to be saved to the heap and resumed later. The async framework allows command bodies to be written in an imperative style. However, an async command will need to be actively cooperative and periodically call coroutine.yield() in loops to yield control back to the command scheduler to let it process other commands. There are also some other additions like priority levels (as opposed to a blanket yes/no for ignoring incoming commands), factories requiring names be provided for commands, and the scheduler tracking all running commands and not just the highest-level groups. However, those changes aren't unique to an async framework, and could just as easily be used in a traditional command framework. --- .github/actions/pregen/action.yml | 5 + BUILD.bazel | 2 + CMakeLists.txt | 2 + README-CMake.md | 1 + commandsv3/.styleguide | 12 + commandsv3/BUILD.bazel | 92 + commandsv3/CMakeLists.txt | 53 + commandsv3/CommandsV3.json | 18 + commandsv3/build.gradle | 49 + commandsv3/commandsv3-config.cmake.in | 15 + commandsv3/generate.bzl | 45 + commandsv3/generate_files.py | 120 ++ .../java/org/wpilib/commands3/DevMain.java | 23 + .../generate/main/java/commandhid.java.jinja | 148 ++ .../button/CommandPS4Controller.java | 447 ++++ .../button/CommandPS5Controller.java | 447 ++++ .../button/CommandStadiaController.java | 451 ++++ .../button/CommandXboxController.java | 435 ++++ .../commands3/proto/ProtobufCommands.java | 1883 +++++++++++++++++ .../java/org/wpilib/commands3/Binding.java | 26 + .../org/wpilib/commands3/BindingScope.java | 52 + .../org/wpilib/commands3/BindingType.java | 46 + .../java/org/wpilib/commands3/Command.java | 416 ++++ .../org/wpilib/commands3/CommandState.java | 100 + .../wpilib/commands3/CommandTraceHelper.java | 73 + .../wpilib/commands3/ConflictDetector.java | 122 ++ .../org/wpilib/commands3/Continuation.java | 230 ++ .../wpilib/commands3/ContinuationScope.java | 54 + .../java/org/wpilib/commands3/Coroutine.java | 377 ++++ .../java/org/wpilib/commands3/Mechanism.java | 165 ++ .../commands3/NeedsExecutionBuilderStage.java | 56 + .../commands3/NeedsNameBuilderStage.java | 56 + .../org/wpilib/commands3/ParallelGroup.java | 107 + .../commands3/ParallelGroupBuilder.java | 147 ++ .../java/org/wpilib/commands3/Scheduler.java | 970 +++++++++ .../org/wpilib/commands3/SchedulerEvent.java | 89 + .../org/wpilib/commands3/SequentialGroup.java | 80 + .../commands3/SequentialGroupBuilder.java | 106 + .../commands3/StagedCommandBuilder.java | 299 +++ .../java/org/wpilib/commands3/Trigger.java | 368 ++++ .../commands3/button/CommandGenericHID.java | 343 +++ .../commands3/button/CommandJoystick.java | 275 +++ .../commands3/button/InternalButton.java | 61 + .../commands3/button/JoystickButton.java | 24 + .../commands3/button/NetworkButton.java | 66 + .../wpilib/commands3/button/POVButton.java | 36 + .../commands3/button/RobotModeTriggers.java | 53 + .../wpilib/commands3/proto/CommandProto.java | 65 + .../commands3/proto/MechanismProto.java | 38 + .../commands3/proto/SchedulerProto.java | 58 + .../src/main/proto/protobuf_commands.proto | 57 + .../org/wpilib/commands3/CommandTestBase.java | 23 + .../commands3/ConflictDetectorTest.java | 70 + .../org/wpilib/commands3/CoroutineTest.java | 174 ++ .../org/wpilib/commands3/NullCommand.java | 24 + .../wpilib/commands3/ParallelGroupTest.java | 233 ++ .../org/wpilib/commands3/PriorityCommand.java | 29 + .../commands3/SchedulerCancellationTests.java | 369 ++++ .../commands3/SchedulerConflictTests.java | 162 ++ .../SchedulerErrorHandlingTests.java | 208 ++ .../SchedulerPriorityLevelTests.java | 64 + .../SchedulerSideloadFunctionTests.java | 100 + .../commands3/SchedulerTelemetryTests.java | 116 + .../org/wpilib/commands3/SchedulerTest.java | 161 ++ .../commands3/SchedulerTimingTests.java | 244 +++ .../wpilib/commands3/SequentialGroupTest.java | 82 + .../commands3/StagedCommandBuilderTest.java | 254 +++ .../org/wpilib/commands3/TriggerTest.java | 247 +++ design-docs/commands-v3.md | 420 ++++ docs/BUILD.bazel | 1 + docs/build.gradle | 3 + settings.gradle | 1 + styleguide/checkstyle-suppressions.xml | 5 +- styleguide/pmd-ruleset.xml | 1 + styleguide/spotbugs-exclude.xml | 5 + wpilib-config.cmake.in | 1 + ...lerTest.java => CommandSchedulerTest.java} | 2 +- 77 files changed, 12259 insertions(+), 3 deletions(-) create mode 100644 commandsv3/.styleguide create mode 100644 commandsv3/BUILD.bazel create mode 100644 commandsv3/CMakeLists.txt create mode 100644 commandsv3/CommandsV3.json create mode 100644 commandsv3/build.gradle create mode 100644 commandsv3/commandsv3-config.cmake.in create mode 100644 commandsv3/generate.bzl create mode 100755 commandsv3/generate_files.py create mode 100644 commandsv3/src/dev/java/org/wpilib/commands3/DevMain.java create mode 100644 commandsv3/src/generate/main/java/commandhid.java.jinja create mode 100644 commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS4Controller.java create mode 100644 commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS5Controller.java create mode 100644 commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandStadiaController.java create mode 100644 commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandXboxController.java create mode 100644 commandsv3/src/generated/main/java/org/wpilib/commands3/proto/ProtobufCommands.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/Binding.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/BindingScope.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/BindingType.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/Command.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/CommandState.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/CommandTraceHelper.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/ConflictDetector.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/Continuation.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/ContinuationScope.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/Coroutine.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/Mechanism.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/NeedsExecutionBuilderStage.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/NeedsNameBuilderStage.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/ParallelGroup.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/ParallelGroupBuilder.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/Scheduler.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/SchedulerEvent.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/SequentialGroup.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/SequentialGroupBuilder.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/StagedCommandBuilder.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/Trigger.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/button/CommandGenericHID.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/button/CommandJoystick.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/button/InternalButton.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/button/JoystickButton.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/button/NetworkButton.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/button/POVButton.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/button/RobotModeTriggers.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/proto/CommandProto.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/proto/MechanismProto.java create mode 100644 commandsv3/src/main/java/org/wpilib/commands3/proto/SchedulerProto.java create mode 100644 commandsv3/src/main/proto/protobuf_commands.proto create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/CommandTestBase.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/ConflictDetectorTest.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/CoroutineTest.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/NullCommand.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/ParallelGroupTest.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/PriorityCommand.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerCancellationTests.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerConflictTests.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerErrorHandlingTests.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerPriorityLevelTests.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerSideloadFunctionTests.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerTelemetryTests.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerTest.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SchedulerTimingTests.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/SequentialGroupTest.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/StagedCommandBuilderTest.java create mode 100644 commandsv3/src/test/java/org/wpilib/commands3/TriggerTest.java create mode 100644 design-docs/commands-v3.md rename wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/{SchedulerTest.java => CommandSchedulerTest.java} (99%) diff --git a/.github/actions/pregen/action.yml b/.github/actions/pregen/action.yml index 1346a79696..e05e19e1ba 100644 --- a/.github/actions/pregen/action.yml +++ b/.github/actions/pregen/action.yml @@ -53,6 +53,11 @@ runs: ./wpimath/generate_quickbuf.py --quickbuf_plugin protoc-gen-quickbuf-1.3.3-linux-x86_64.exe shell: bash + - name: Regenerate Commands v3 + run: | + ./commandsv3/generate_files.py --quickbuf_plugin protoc-gen-quickbuf-1.3.3-linux-x86_64.exe + shell: bash + - name: Regenerate wpiunits run: ./wpiunits/generate_units.py shell: bash diff --git a/BUILD.bazel b/BUILD.bazel index dbd1720ce7..157474099b 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -51,6 +51,7 @@ write_source_files( "//wpilibj:write_wpilibj", "//wpilibjExamples:write_example_project_list", "//wpilibNewCommands:write_wpilib_new_commands", + "//commandsv3:write_commandsv3", "//wpimath:write_wpimath", "//wpiunits:write_wpiunits", "//wpiutil:write_wpiutil", @@ -100,6 +101,7 @@ publish_all( "//wpigui:wpigui-cpp_publish.publish", "//wpilibNewCommands:wpilibNewCommands-cpp_publish.publish", "//wpilibNewCommands:wpilibNewCommands-java_publish.publish", + "//commandsv3:commandsv3-java_publish.publish", "//wpilibc:wpilibc-cpp_publish.publish", "//wpilibcExamples:commands_publish.publish", "//wpilibcExamples:examples_publish.publish", diff --git a/CMakeLists.txt b/CMakeLists.txt index 71fce23497..6d6d4c80c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -346,12 +346,14 @@ endif() if(WITH_WPILIB) set(APRILTAG_DEP_REPLACE "find_dependency(apriltag)") + set(COMMANDSV3_DEP_REPLACE "find_dependency(commandsv3)") set(WPILIBC_DEP_REPLACE "find_dependency(wpilibc)") set(WPILIBJ_DEP_REPLACE "find_dependency(wpilibj)") set(WPILIBNEWCOMMANDS_DEP_REPLACE "find_dependency(wpilibNewCommands)") add_subdirectory(apriltag) add_subdirectory(wpilibj) add_subdirectory(wpilibc) + add_subdirectory(commandsv3) # must be after wpilibj add_subdirectory(wpilibNewCommands) add_subdirectory(romiVendordep) add_subdirectory(xrpVendordep) diff --git a/README-CMake.md b/README-CMake.md index eaa954d2d1..a91885f19a 100644 --- a/README-CMake.md +++ b/README-CMake.md @@ -5,6 +5,7 @@ WPILib is normally built with Gradle, however for some systems, such as Linux ba ## Libraries that get built * apriltag * cameraserver +* commandsv3 * cscore * fieldImages * hal (simulation HAL only) diff --git a/commandsv3/.styleguide b/commandsv3/.styleguide new file mode 100644 index 0000000000..0f2122eeb7 --- /dev/null +++ b/commandsv3/.styleguide @@ -0,0 +1,12 @@ +modifiableFileExclude { + \.patch$ +} + +generatedFileExclude { + src/generated/main/java/org/wpilib/commands3/button/ + src/generated/main/java/org/wpilib/commands3/proto/ +} + +repoRootNameOverride { + commandsv3 +} diff --git a/commandsv3/BUILD.bazel b/commandsv3/BUILD.bazel new file mode 100644 index 0000000000..2916630565 --- /dev/null +++ b/commandsv3/BUILD.bazel @@ -0,0 +1,92 @@ +load("@allwpilib_pip_deps//:requirements.bzl", "requirement") +load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files") +load("@rules_java//java:defs.bzl", "java_binary") +load("@rules_python//python:defs.bzl", "py_binary") +load("//commandsv3:generate.bzl", "generate_commandsv3") +load("//shared/bazel/rules:java_rules.bzl", "wpilib_java_junit5_test", "wpilib_java_library") + +py_binary( + name = "generate_files", + srcs = ["generate_files.py"], + target_compatible_with = select({ + "@rules_bzlmodrio_toolchains//constraints/is_systemcore:systemcore": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + deps = [requirement("jinja2")], +) + +filegroup( + name = "templates", + srcs = glob(["src/generate/main/**"]) + [ + "//wpilibj:hid_schema", + ], +) + +generate_commandsv3( + name = "generate_commandsv3", +) + +write_source_files( + name = "write_commandsv3", + files = { + "src/generated": ":generate_commandsv3", + }, + suggested_update_target = "//:write_all", + tags = ["pregeneration"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "generated_java", + srcs = glob(["src/generated/main/java/**/*.java"]), +) + +wpilib_java_library( + name = "commandsv3-java", + srcs = glob(["src/main/java/**/*.java"]) + [":generated_java"], + exported_plugins = ["//javacPlugin:plugin"], + maven_artifact_name = "commands3-java", + maven_group_id = "org.wpilib.commands3", + plugins = ["//javacPlugin:plugin"], + visibility = ["//visibility:public"], + deps = [ + "//cscore:cscore-java", + "//hal:hal-java", + "//ntcore:ntcore-java", + "//wpiannotations", + "//wpilibj:wpilibj-java", + "//wpimath:wpimath-java", + "//wpinet:wpinet-java", + "//wpiunits:wpiunits-java", + "//wpiutil:wpiutil-java", + "@maven//:us_hebi_quickbuf_quickbuf_runtime", + ], +) + +wpilib_java_junit5_test( + name = "commandsv3-java-test", + srcs = glob(["**/*.java"]), + deps = [ + ":commandsv3-java", + "//hal:hal-java", + "//ntcore:ntcore-java", + "//wpiannotations", + "//wpilibj:wpilibj-java", + "//wpimath:wpimath-java", + "//wpiunits:wpiunits-java", + "//wpiutil:wpiutil-java", + "@maven//:us_hebi_quickbuf_quickbuf_runtime", + ], +) + +java_binary( + name = "DevMain-Java", + srcs = ["src/dev/java/org/wpilib/commands3/DevMain.java"], + main_class = "org.wpilib.commands3.DevMain", + deps = [ + "//hal:hal-java", + "//ntcore:ntcore-java", + "//wpimath:wpimath-java", + "//wpiutil:wpiutil-java", + ], +) diff --git a/commandsv3/CMakeLists.txt b/commandsv3/CMakeLists.txt new file mode 100644 index 0000000000..d50c6a4971 --- /dev/null +++ b/commandsv3/CMakeLists.txt @@ -0,0 +1,53 @@ +project(commandsv3) + +include(SubDirList) +include(CompileWarnings) +include(AddTest) + +if(WITH_JAVA) + include(UseJava) + + file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java src/generated/main/java/*.java) + file(GLOB QUICKBUF_JAR ${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/*.jar) + + add_jar( + commandsv3_jar + ${JAVA_SOURCES} + INCLUDE_JARS + datalog_jar + hal_jar + ntcore_jar + cscore_jar + cameraserver_jar + wpiannotations_jar + wpimath_jar + wpiunits_jar + wpiutil_jar + wpilibj_jar + ${QUICKBUF_JAR} + OUTPUT_NAME commandsv3 + OUTPUT_DIR ${WPILIB_BINARY_DIR}/${java_lib_dest} + ) + + install_jar(commandsv3_jar DESTINATION ${java_lib_dest}) + install_jar_exports( + TARGETS commandsv3_jar + FILE commandsv3_jar.cmake + DESTINATION share/commandsv3 + ) +endif() + +if(WITH_JAVA_SOURCE) + include(UseJava) + include(CreateSourceJar) + add_source_jar( + commandsv3_src_jar + BASE_DIRECTORIES + ${CMAKE_CURRENT_SOURCE_DIR}/src/main/java + ${CMAKE_CURRENT_SOURCE_DIR}/src/generated/main/java + OUTPUT_NAME commandsv3-sources + OUTPUT_DIR ${WPILIB_BINARY_DIR}/${java_lib_dest} + ) + set_property(TARGET commandsv3_src_jar PROPERTY FOLDER "java") + install_jar(commandsv3_src_jar DESTINATION ${java_lib_dest}) +endif() diff --git a/commandsv3/CommandsV3.json b/commandsv3/CommandsV3.json new file mode 100644 index 0000000000..72c540ef84 --- /dev/null +++ b/commandsv3/CommandsV3.json @@ -0,0 +1,18 @@ +{ + "fileName": "CommandsV3.json", + "name": "Commands v3", + "version": "1.0.0", + "uuid": "4decdc05-a056-46cf-9561-39449bbb0130", + "frcYear": "2027_alpha1", + "mavenUrls": [], + "jsonUrl": "", + "javaDependencies": [ + { + "groupId": "org.wpilib.commands3", + "artifactId": "commands3-java", + "version": "wpilib" + } + ], + "jniDependencies": [], + "cppDependencies": [] +} diff --git a/commandsv3/build.gradle b/commandsv3/build.gradle new file mode 100644 index 0000000000..b097060d1d --- /dev/null +++ b/commandsv3/build.gradle @@ -0,0 +1,49 @@ +ext { + useJava = true + useCpp = false + baseId = 'commands3' + groupId = 'org.wpilib' + + nativeName = 'commands3' + devMain = 'org.wpilib.commands3.DevMain' +} + +apply from: "${rootDir}/shared/java/javacommon.gradle" + +evaluationDependsOn(':wpiutil') +evaluationDependsOn(':ntcore') +evaluationDependsOn(':hal') +evaluationDependsOn(':wpimath') +evaluationDependsOn(':wpilibj') + +dependencies { + annotationProcessor project(':javacPlugin') + implementation project(':wpiannotations') + implementation project(':wpiutil') + implementation project(':wpinet') + implementation project(':ntcore') + implementation project(':hal') + implementation project(':wpimath') + implementation project(':wpilibj') + api("us.hebi.quickbuf:quickbuf-runtime:1.4") + testAnnotationProcessor project(':javacPlugin') + testImplementation 'org.mockito:mockito-core:4.1.0' +} + +sourceSets.main.java.srcDir "${projectDir}/src/generated/main/java" +sourceSets.main.resources.srcDir "${projectDir}/src/main/proto" + +test { + testLogging { + outputs.upToDateWhen {false} + showStandardStreams = true + } + + // For reflective access to the continuation classes + jvmArgs += [ + '--add-opens', + 'java.base/jdk.internal.vm=ALL-UNNAMED', + '--add-opens', + 'java.base/java.lang=ALL-UNNAMED', + ] +} diff --git a/commandsv3/commandsv3-config.cmake.in b/commandsv3/commandsv3-config.cmake.in new file mode 100644 index 0000000000..30194c4250 --- /dev/null +++ b/commandsv3/commandsv3-config.cmake.in @@ -0,0 +1,15 @@ +include(CMakeFindDependencyMacro) +@WPIUTIL_DEP_REPLACE@ +@DATALOG_DEP_REPLACE@ +@NTCORE_DEP_REPLACE@ +@CSCORE_DEP_REPLACE@ +@CAMERASERVER_DEP_REPLACE@ +@HAL_DEP_REPLACE@ +@WPILIBC_DEP_REPLACE@ +@WPIMATH_DEP_REPLACE@ + +@FILENAME_DEP_REPLACE@ +include(${SELF_DIR}/commandsv3.cmake) +if(@WITH_JAVA@) + include(${SELF_DIR}/commandsv3_jar.cmake) +endif() diff --git a/commandsv3/generate.bzl b/commandsv3/generate.bzl new file mode 100644 index 0000000000..4c462cc341 --- /dev/null +++ b/commandsv3/generate.bzl @@ -0,0 +1,45 @@ +def __generate_commandsv3_impl(ctx): + """ + Custom rule used to create the commandsv3 pre-generated files. See `./README-Bazel.md` for the reasoning. + """ + output_dir = ctx.actions.declare_directory("_gendir") + + args = ctx.actions.args() + args.add("--output_directory", output_dir.path) + args.add("--template_root", "commandsv3/src/generate") + args.add("--protoc", ctx.executable._protoc) + args.add("--quickbuf_plugin", ctx.executable._quickbuf) + + ctx.actions.run( + inputs = ctx.attr._templates.files, + outputs = [output_dir], + executable = ctx.executable._tool, + arguments = [args], + tools = [ctx.executable._protoc, ctx.executable._quickbuf], + ) + + return [DefaultInfo(files = depset([output_dir]))] + +generate_commandsv3 = rule( + implementation = __generate_commandsv3_impl, + attrs = { + "_protoc": attr.label( + default = Label("@com_google_protobuf//:protoc"), + cfg = "exec", + executable = True, + ), + "_quickbuf": attr.label( + default = Label("//:quickbuf_protoc"), + cfg = "exec", + executable = True, + ), + "_templates": attr.label( + default = Label("//commandsv3:templates"), + ), + "_tool": attr.label( + default = Label("//commandsv3:generate_files"), + cfg = "exec", + executable = True, + ), + }, +) diff --git a/commandsv3/generate_files.py b/commandsv3/generate_files.py new file mode 100755 index 0000000000..f6c1cb20c0 --- /dev/null +++ b/commandsv3/generate_files.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +# Copyright (c) FIRST and other WPILib contributors. +# Open Source Software; you can modify and/or share it under the terms of +# the WPILib BSD license file in the root directory of this project. + +import argparse +import json +import subprocess +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + + +def write_controller_file(output_dir: Path, controller_name: str, contents: str): + output_dir.mkdir(parents=True, exist_ok=True) + output_file = output_dir / controller_name + output_file.write_text(contents, encoding="utf-8", newline="\n") + print("Writing to ", output_file) + + +def generate_hids(output_directory: Path, template_directory: Path, schema_file: Path): + with schema_file.open(encoding="utf-8") as f: + controllers = json.load(f) + + # Java files + java_subdirectory = "main/java/org/wpilib/commands3/button" + env = Environment( + loader=FileSystemLoader(template_directory / "main/java"), + autoescape=False, + keep_trailing_newline=True, + ) + root_path = output_directory / java_subdirectory + template = env.get_template("commandhid.java.jinja") + for controller in controllers: + controllerName = f"Command{controller['ConsoleName']}Controller.java" + output = template.render(controller) + write_controller_file(root_path, controllerName, output) + + +def generate_quickbuf( + protoc, quickbuf_plugin: Path, output_directory: Path, proto_dir: Path +): + proto_files = proto_dir.glob("*.proto") + for path in proto_files: + absolute_filename = path.absolute() + args = [protoc] + if quickbuf_plugin: + # Optional on macOS if using protoc-quickbuf + args += [f"--plugin=protoc-gen-quickbuf={quickbuf_plugin}"] + args += [ + f"--quickbuf_out=gen_descriptors=true:{output_directory.absolute()}", + f"-I{absolute_filename.parent}", + absolute_filename, + ] + subprocess.check_call(args) + java_files = (output_directory / "org/wpilib/commands3/proto").glob("*.java") + for java_file in java_files: + with (java_file).open(encoding="utf-8") as f: + content = f.read() + + java_file.write_text( + "// Copyright (c) FIRST and other WPILib contributors.\n// Open Source Software; you can modify and/or share it under the terms of\n// the WPILib BSD license file in the root directory of this project.\n" + + content, + encoding="utf-8", + newline="\n", + ) + + +def main(): + script_path = Path(__file__).resolve() + dirname = script_path.parent + + parser = argparse.ArgumentParser() + parser.add_argument( + "--output_directory", + help="Optional. If set, will output the generated files to this directory, otherwise it will use a path relative to the script", + default=dirname / "src/generated", + type=Path, + ) + parser.add_argument( + "--template_root", + help="Optional. If set, will use this directory as the root for the jinja templates", + default=dirname / "src/generate", + type=Path, + ) + parser.add_argument( + "--schema_file", + help="Optional. If set, will use this file for the joystick schema", + default="wpilibj/src/generate/hids.json", + type=Path, + ) + parser.add_argument( + "--protoc", + help="Protoc executable command", + default="protoc", + ) + parser.add_argument( + "--quickbuf_plugin", + help="Path to the quickbuf protoc plugin", + ) + parser.add_argument( + "--proto_directory", + help="Optional. If set, will use this directory to glob for protobuf files", + default=dirname / "src/main/proto", + type=Path, + ) + args = parser.parse_args() + + generate_hids(args.output_directory, args.template_root, args.schema_file) + generate_quickbuf( + args.protoc, + args.quickbuf_plugin, + args.output_directory / "main/java", + args.proto_directory, + ) + + +if __name__ == "__main__": + main() diff --git a/commandsv3/src/dev/java/org/wpilib/commands3/DevMain.java b/commandsv3/src/dev/java/org/wpilib/commands3/DevMain.java new file mode 100644 index 0000000000..8a92b7b09d --- /dev/null +++ b/commandsv3/src/dev/java/org/wpilib/commands3/DevMain.java @@ -0,0 +1,23 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import edu.wpi.first.hal.HALUtil; +import edu.wpi.first.networktables.NetworkTablesJNI; +import edu.wpi.first.util.CombinedRuntimeLoader; + +/** Dev main. */ +public final class DevMain { + /** Main entry point. */ + public static void main(String[] args) { + System.out.println("Commands V3 DevMain"); + + System.out.println(CombinedRuntimeLoader.getPlatformPath()); + System.out.println(NetworkTablesJNI.now()); + System.out.println(HALUtil.getHALRuntimeType()); + } + + private DevMain() {} +} diff --git a/commandsv3/src/generate/main/java/commandhid.java.jinja b/commandsv3/src/generate/main/java/commandhid.java.jinja new file mode 100644 index 0000000000..17f538a762 --- /dev/null +++ b/commandsv3/src/generate/main/java/commandhid.java.jinja @@ -0,0 +1,148 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +// THIS FILE WAS AUTO-GENERATED BY ./commandsv3/generate_files.py. DO NOT MODIFY +{% macro capitalize_first(string) -%} +{{ string[0]|capitalize + string[1:] }} +{%- endmacro %} +package org.wpilib.commands3.button; + +import edu.wpi.first.wpilibj.{{ ConsoleName }}Controller; +import edu.wpi.first.wpilibj.event.EventLoop; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.Trigger; + +/** + * A version of {@link {{ ConsoleName }}Controller} with {@link Trigger} factories for command-based. + * + * @see {{ ConsoleName }}Controller + */ +@SuppressWarnings("MethodName") +public class Command{{ ConsoleName }}Controller extends CommandGenericHID { + private final {{ ConsoleName }}Controller m_hid; + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the {@link Scheduler#getDefault() default scheduler} using its default event loop. + * + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public Command{{ ConsoleName }}Controller(int port) { + super(port); + m_hid = new {{ ConsoleName }}Controller(port); + } + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the given scheduler using its default event loop. + * + * @param scheduler The scheduler that should execute the triggered commands. + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public Command{{ ConsoleName }}Controller(Scheduler scheduler, int port) { + super(scheduler, port); + m_hid = new {{ ConsoleName }}Controller(port); + } + + /** + * Get the underlying GenericHID object. + * + * @return the wrapped GenericHID object + */ + @Override + public {{ ConsoleName }}Controller getHID() { + return m_hid; + } +{% for button in buttons %} + /** + * Constructs a Trigger instance around the {{ button.DocName|default(button.name) }} button's digital signal. + * + * @return a Trigger instance representing the {{ button.DocName|default(button.name) }} button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #{{ button.name }}(EventLoop) + */ + public Trigger {{ button.name }}() { + return {{ button.name }}(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the {{ button.DocName|default(button.name) }} button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the {{ button.DocName|default(button.name) }} button's digital signal attached + * to the given loop. + */ + public Trigger {{ button.name }}(EventLoop loop) { + return button({{ ConsoleName }}Controller.Button.k{{ capitalize_first(button.name) }}.value, loop); + } +{% endfor -%} +{% for trigger in triggers -%} +{% if trigger.UseThresholdMethods %} + /** + * Constructs a Trigger instance around the axis value of the {{ trigger.DocName }}. The returned + * trigger will be true when the axis value is greater than {@code threshold}. + * + * @param threshold the minimum axis value for the returned {@link Trigger} to be true. This value + * should be in the range [0, 1] where 0 is the unpressed state of the axis. + * @param loop the event loop instance to attach the Trigger to. + * @return a Trigger instance that is true when the {{ trigger.DocName }}'s axis exceeds the provided + * threshold, attached to the given event loop + */ + public Trigger {{ trigger.name }}(double threshold, EventLoop loop) { + return axisGreaterThan({{ ConsoleName }}Controller.Axis.k{{ capitalize_first(trigger.name) }}.value, threshold, loop); + } + + /** + * Constructs a Trigger instance around the axis value of the {{ trigger.DocName }}. The returned + * trigger will be true when the axis value is greater than {@code threshold}. + * + * @param threshold the minimum axis value for the returned {@link Trigger} to be true. This value + * should be in the range [0, 1] where 0 is the unpressed state of the axis. + * @return a Trigger instance that is true when the {{ trigger.DocName }}'s axis exceeds the provided + * threshold, attached to the {@link Scheduler#getDefaultEventLoop() default scheduler event + * loop} on the scheduler passed to the controller's constructor, or the {@link + * Scheduler#getDefault default scheduler} if a scheduler was not explicitly provided. + */ + public Trigger {{ trigger.name }}(double threshold) { + return {{ trigger.name }}(threshold, getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the axis value of the {{ trigger.DocName }}. The returned trigger + * will be true when the axis value is greater than 0.5. + * + * @return a Trigger instance that is true when the {{ trigger.DocName }}'s axis exceeds 0.5, attached to + * the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + */ + public Trigger {{ trigger.name }}() { + return {{ trigger.name }}(0.5); + } +{% endif -%} +{% endfor -%} +{% for stick in sticks %} + /** + * Get the {{ stick.NameParts[1] }} axis value of {{ stick.NameParts[0] }} side of the controller. {{ stick.PositiveDirection }} is positive. + * + * @return The axis value. + */ + public double get{{ stick.NameParts|map("capitalize")|join }}() { + return m_hid.get{{ stick.NameParts|map("capitalize")|join }}(); + } +{% endfor -%} +{% for trigger in triggers %} + /** + * Get the {{ trigger.DocName }} axis value of the controller. Note that this axis is bound to the + * range of [0, 1] as opposed to the usual [-1, 1]. + * + * @return The axis value. + */ + public double get{{ capitalize_first(trigger.name) }}Axis() { + return m_hid.get{{ capitalize_first(trigger.name) }}Axis(); + } +{% endfor -%} +} diff --git a/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS4Controller.java b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS4Controller.java new file mode 100644 index 0000000000..e48b2358e0 --- /dev/null +++ b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS4Controller.java @@ -0,0 +1,447 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +// THIS FILE WAS AUTO-GENERATED BY ./commandsv3/generate_files.py. DO NOT MODIFY + +package org.wpilib.commands3.button; + +import edu.wpi.first.wpilibj.PS4Controller; +import edu.wpi.first.wpilibj.event.EventLoop; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.Trigger; + +/** + * A version of {@link PS4Controller} with {@link Trigger} factories for command-based. + * + * @see PS4Controller + */ +@SuppressWarnings("MethodName") +public class CommandPS4Controller extends CommandGenericHID { + private final PS4Controller m_hid; + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the {@link Scheduler#getDefault() default scheduler} using its default event loop. + * + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandPS4Controller(int port) { + super(port); + m_hid = new PS4Controller(port); + } + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the given scheduler using its default event loop. + * + * @param scheduler The scheduler that should execute the triggered commands. + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandPS4Controller(Scheduler scheduler, int port) { + super(scheduler, port); + m_hid = new PS4Controller(port); + } + + /** + * Get the underlying GenericHID object. + * + * @return the wrapped GenericHID object + */ + @Override + public PS4Controller getHID() { + return m_hid; + } + + /** + * Constructs a Trigger instance around the square button's digital signal. + * + * @return a Trigger instance representing the square button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #square(EventLoop) + */ + public Trigger square() { + return square(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the square button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the square button's digital signal attached + * to the given loop. + */ + public Trigger square(EventLoop loop) { + return button(PS4Controller.Button.kSquare.value, loop); + } + + /** + * Constructs a Trigger instance around the cross button's digital signal. + * + * @return a Trigger instance representing the cross button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #cross(EventLoop) + */ + public Trigger cross() { + return cross(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the cross button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the cross button's digital signal attached + * to the given loop. + */ + public Trigger cross(EventLoop loop) { + return button(PS4Controller.Button.kCross.value, loop); + } + + /** + * Constructs a Trigger instance around the circle button's digital signal. + * + * @return a Trigger instance representing the circle button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #circle(EventLoop) + */ + public Trigger circle() { + return circle(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the circle button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the circle button's digital signal attached + * to the given loop. + */ + public Trigger circle(EventLoop loop) { + return button(PS4Controller.Button.kCircle.value, loop); + } + + /** + * Constructs a Trigger instance around the triangle button's digital signal. + * + * @return a Trigger instance representing the triangle button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #triangle(EventLoop) + */ + public Trigger triangle() { + return triangle(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the triangle button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the triangle button's digital signal attached + * to the given loop. + */ + public Trigger triangle(EventLoop loop) { + return button(PS4Controller.Button.kTriangle.value, loop); + } + + /** + * Constructs a Trigger instance around the left trigger 1 button's digital signal. + * + * @return a Trigger instance representing the left trigger 1 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #L1(EventLoop) + */ + public Trigger L1() { + return L1(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left trigger 1 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left trigger 1 button's digital signal attached + * to the given loop. + */ + public Trigger L1(EventLoop loop) { + return button(PS4Controller.Button.kL1.value, loop); + } + + /** + * Constructs a Trigger instance around the right trigger 1 button's digital signal. + * + * @return a Trigger instance representing the right trigger 1 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #R1(EventLoop) + */ + public Trigger R1() { + return R1(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right trigger 1 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right trigger 1 button's digital signal attached + * to the given loop. + */ + public Trigger R1(EventLoop loop) { + return button(PS4Controller.Button.kR1.value, loop); + } + + /** + * Constructs a Trigger instance around the left trigger 2 button's digital signal. + * + * @return a Trigger instance representing the left trigger 2 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #L2(EventLoop) + */ + public Trigger L2() { + return L2(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left trigger 2 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left trigger 2 button's digital signal attached + * to the given loop. + */ + public Trigger L2(EventLoop loop) { + return button(PS4Controller.Button.kL2.value, loop); + } + + /** + * Constructs a Trigger instance around the right trigger 2 button's digital signal. + * + * @return a Trigger instance representing the right trigger 2 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #R2(EventLoop) + */ + public Trigger R2() { + return R2(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right trigger 2 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right trigger 2 button's digital signal attached + * to the given loop. + */ + public Trigger R2(EventLoop loop) { + return button(PS4Controller.Button.kR2.value, loop); + } + + /** + * Constructs a Trigger instance around the share button's digital signal. + * + * @return a Trigger instance representing the share button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #share(EventLoop) + */ + public Trigger share() { + return share(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the share button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the share button's digital signal attached + * to the given loop. + */ + public Trigger share(EventLoop loop) { + return button(PS4Controller.Button.kShare.value, loop); + } + + /** + * Constructs a Trigger instance around the options button's digital signal. + * + * @return a Trigger instance representing the options button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #options(EventLoop) + */ + public Trigger options() { + return options(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the options button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the options button's digital signal attached + * to the given loop. + */ + public Trigger options(EventLoop loop) { + return button(PS4Controller.Button.kOptions.value, loop); + } + + /** + * Constructs a Trigger instance around the L3 (left stick) button's digital signal. + * + * @return a Trigger instance representing the L3 (left stick) button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #L3(EventLoop) + */ + public Trigger L3() { + return L3(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the L3 (left stick) button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the L3 (left stick) button's digital signal attached + * to the given loop. + */ + public Trigger L3(EventLoop loop) { + return button(PS4Controller.Button.kL3.value, loop); + } + + /** + * Constructs a Trigger instance around the R3 (right stick) button's digital signal. + * + * @return a Trigger instance representing the R3 (right stick) button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #R3(EventLoop) + */ + public Trigger R3() { + return R3(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the R3 (right stick) button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the R3 (right stick) button's digital signal attached + * to the given loop. + */ + public Trigger R3(EventLoop loop) { + return button(PS4Controller.Button.kR3.value, loop); + } + + /** + * Constructs a Trigger instance around the PlayStation button's digital signal. + * + * @return a Trigger instance representing the PlayStation button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #PS(EventLoop) + */ + public Trigger PS() { + return PS(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the PlayStation button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the PlayStation button's digital signal attached + * to the given loop. + */ + public Trigger PS(EventLoop loop) { + return button(PS4Controller.Button.kPS.value, loop); + } + + /** + * Constructs a Trigger instance around the touchpad button's digital signal. + * + * @return a Trigger instance representing the touchpad button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #touchpad(EventLoop) + */ + public Trigger touchpad() { + return touchpad(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the touchpad button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the touchpad button's digital signal attached + * to the given loop. + */ + public Trigger touchpad(EventLoop loop) { + return button(PS4Controller.Button.kTouchpad.value, loop); + } + + /** + * Get the X axis value of left side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getLeftX() { + return m_hid.getLeftX(); + } + + /** + * Get the Y axis value of left side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getLeftY() { + return m_hid.getLeftY(); + } + + /** + * Get the X axis value of right side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getRightX() { + return m_hid.getRightX(); + } + + /** + * Get the Y axis value of right side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getRightY() { + return m_hid.getRightY(); + } + + /** + * Get the left trigger 2 axis value of the controller. Note that this axis is bound to the + * range of [0, 1] as opposed to the usual [-1, 1]. + * + * @return The axis value. + */ + public double getL2Axis() { + return m_hid.getL2Axis(); + } + + /** + * Get the right trigger 2 axis value of the controller. Note that this axis is bound to the + * range of [0, 1] as opposed to the usual [-1, 1]. + * + * @return The axis value. + */ + public double getR2Axis() { + return m_hid.getR2Axis(); + } +} diff --git a/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS5Controller.java b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS5Controller.java new file mode 100644 index 0000000000..5ee032fdb4 --- /dev/null +++ b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandPS5Controller.java @@ -0,0 +1,447 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +// THIS FILE WAS AUTO-GENERATED BY ./commandsv3/generate_files.py. DO NOT MODIFY + +package org.wpilib.commands3.button; + +import edu.wpi.first.wpilibj.PS5Controller; +import edu.wpi.first.wpilibj.event.EventLoop; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.Trigger; + +/** + * A version of {@link PS5Controller} with {@link Trigger} factories for command-based. + * + * @see PS5Controller + */ +@SuppressWarnings("MethodName") +public class CommandPS5Controller extends CommandGenericHID { + private final PS5Controller m_hid; + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the {@link Scheduler#getDefault() default scheduler} using its default event loop. + * + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandPS5Controller(int port) { + super(port); + m_hid = new PS5Controller(port); + } + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the given scheduler using its default event loop. + * + * @param scheduler The scheduler that should execute the triggered commands. + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandPS5Controller(Scheduler scheduler, int port) { + super(scheduler, port); + m_hid = new PS5Controller(port); + } + + /** + * Get the underlying GenericHID object. + * + * @return the wrapped GenericHID object + */ + @Override + public PS5Controller getHID() { + return m_hid; + } + + /** + * Constructs a Trigger instance around the square button's digital signal. + * + * @return a Trigger instance representing the square button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #square(EventLoop) + */ + public Trigger square() { + return square(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the square button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the square button's digital signal attached + * to the given loop. + */ + public Trigger square(EventLoop loop) { + return button(PS5Controller.Button.kSquare.value, loop); + } + + /** + * Constructs a Trigger instance around the cross button's digital signal. + * + * @return a Trigger instance representing the cross button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #cross(EventLoop) + */ + public Trigger cross() { + return cross(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the cross button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the cross button's digital signal attached + * to the given loop. + */ + public Trigger cross(EventLoop loop) { + return button(PS5Controller.Button.kCross.value, loop); + } + + /** + * Constructs a Trigger instance around the circle button's digital signal. + * + * @return a Trigger instance representing the circle button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #circle(EventLoop) + */ + public Trigger circle() { + return circle(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the circle button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the circle button's digital signal attached + * to the given loop. + */ + public Trigger circle(EventLoop loop) { + return button(PS5Controller.Button.kCircle.value, loop); + } + + /** + * Constructs a Trigger instance around the triangle button's digital signal. + * + * @return a Trigger instance representing the triangle button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #triangle(EventLoop) + */ + public Trigger triangle() { + return triangle(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the triangle button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the triangle button's digital signal attached + * to the given loop. + */ + public Trigger triangle(EventLoop loop) { + return button(PS5Controller.Button.kTriangle.value, loop); + } + + /** + * Constructs a Trigger instance around the left trigger 1 button's digital signal. + * + * @return a Trigger instance representing the left trigger 1 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #L1(EventLoop) + */ + public Trigger L1() { + return L1(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left trigger 1 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left trigger 1 button's digital signal attached + * to the given loop. + */ + public Trigger L1(EventLoop loop) { + return button(PS5Controller.Button.kL1.value, loop); + } + + /** + * Constructs a Trigger instance around the right trigger 1 button's digital signal. + * + * @return a Trigger instance representing the right trigger 1 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #R1(EventLoop) + */ + public Trigger R1() { + return R1(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right trigger 1 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right trigger 1 button's digital signal attached + * to the given loop. + */ + public Trigger R1(EventLoop loop) { + return button(PS5Controller.Button.kR1.value, loop); + } + + /** + * Constructs a Trigger instance around the left trigger 2 button's digital signal. + * + * @return a Trigger instance representing the left trigger 2 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #L2(EventLoop) + */ + public Trigger L2() { + return L2(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left trigger 2 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left trigger 2 button's digital signal attached + * to the given loop. + */ + public Trigger L2(EventLoop loop) { + return button(PS5Controller.Button.kL2.value, loop); + } + + /** + * Constructs a Trigger instance around the right trigger 2 button's digital signal. + * + * @return a Trigger instance representing the right trigger 2 button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #R2(EventLoop) + */ + public Trigger R2() { + return R2(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right trigger 2 button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right trigger 2 button's digital signal attached + * to the given loop. + */ + public Trigger R2(EventLoop loop) { + return button(PS5Controller.Button.kR2.value, loop); + } + + /** + * Constructs a Trigger instance around the create button's digital signal. + * + * @return a Trigger instance representing the create button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #create(EventLoop) + */ + public Trigger create() { + return create(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the create button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the create button's digital signal attached + * to the given loop. + */ + public Trigger create(EventLoop loop) { + return button(PS5Controller.Button.kCreate.value, loop); + } + + /** + * Constructs a Trigger instance around the options button's digital signal. + * + * @return a Trigger instance representing the options button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #options(EventLoop) + */ + public Trigger options() { + return options(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the options button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the options button's digital signal attached + * to the given loop. + */ + public Trigger options(EventLoop loop) { + return button(PS5Controller.Button.kOptions.value, loop); + } + + /** + * Constructs a Trigger instance around the L3 (left stick) button's digital signal. + * + * @return a Trigger instance representing the L3 (left stick) button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #L3(EventLoop) + */ + public Trigger L3() { + return L3(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the L3 (left stick) button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the L3 (left stick) button's digital signal attached + * to the given loop. + */ + public Trigger L3(EventLoop loop) { + return button(PS5Controller.Button.kL3.value, loop); + } + + /** + * Constructs a Trigger instance around the R3 (right stick) button's digital signal. + * + * @return a Trigger instance representing the R3 (right stick) button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #R3(EventLoop) + */ + public Trigger R3() { + return R3(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the R3 (right stick) button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the R3 (right stick) button's digital signal attached + * to the given loop. + */ + public Trigger R3(EventLoop loop) { + return button(PS5Controller.Button.kR3.value, loop); + } + + /** + * Constructs a Trigger instance around the PlayStation button's digital signal. + * + * @return a Trigger instance representing the PlayStation button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #PS(EventLoop) + */ + public Trigger PS() { + return PS(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the PlayStation button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the PlayStation button's digital signal attached + * to the given loop. + */ + public Trigger PS(EventLoop loop) { + return button(PS5Controller.Button.kPS.value, loop); + } + + /** + * Constructs a Trigger instance around the touchpad button's digital signal. + * + * @return a Trigger instance representing the touchpad button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #touchpad(EventLoop) + */ + public Trigger touchpad() { + return touchpad(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the touchpad button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the touchpad button's digital signal attached + * to the given loop. + */ + public Trigger touchpad(EventLoop loop) { + return button(PS5Controller.Button.kTouchpad.value, loop); + } + + /** + * Get the X axis value of left side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getLeftX() { + return m_hid.getLeftX(); + } + + /** + * Get the Y axis value of left side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getLeftY() { + return m_hid.getLeftY(); + } + + /** + * Get the X axis value of right side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getRightX() { + return m_hid.getRightX(); + } + + /** + * Get the Y axis value of right side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getRightY() { + return m_hid.getRightY(); + } + + /** + * Get the left trigger 2 axis value of the controller. Note that this axis is bound to the + * range of [0, 1] as opposed to the usual [-1, 1]. + * + * @return The axis value. + */ + public double getL2Axis() { + return m_hid.getL2Axis(); + } + + /** + * Get the right trigger 2 axis value of the controller. Note that this axis is bound to the + * range of [0, 1] as opposed to the usual [-1, 1]. + * + * @return The axis value. + */ + public double getR2Axis() { + return m_hid.getR2Axis(); + } +} diff --git a/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandStadiaController.java b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandStadiaController.java new file mode 100644 index 0000000000..3b9cdac1c5 --- /dev/null +++ b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandStadiaController.java @@ -0,0 +1,451 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +// THIS FILE WAS AUTO-GENERATED BY ./commandsv3/generate_files.py. DO NOT MODIFY + +package org.wpilib.commands3.button; + +import edu.wpi.first.wpilibj.StadiaController; +import edu.wpi.first.wpilibj.event.EventLoop; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.Trigger; + +/** + * A version of {@link StadiaController} with {@link Trigger} factories for command-based. + * + * @see StadiaController + */ +@SuppressWarnings("MethodName") +public class CommandStadiaController extends CommandGenericHID { + private final StadiaController m_hid; + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the {@link Scheduler#getDefault() default scheduler} using its default event loop. + * + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandStadiaController(int port) { + super(port); + m_hid = new StadiaController(port); + } + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the given scheduler using its default event loop. + * + * @param scheduler The scheduler that should execute the triggered commands. + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandStadiaController(Scheduler scheduler, int port) { + super(scheduler, port); + m_hid = new StadiaController(port); + } + + /** + * Get the underlying GenericHID object. + * + * @return the wrapped GenericHID object + */ + @Override + public StadiaController getHID() { + return m_hid; + } + + /** + * Constructs a Trigger instance around the A button's digital signal. + * + * @return a Trigger instance representing the A button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #a(EventLoop) + */ + public Trigger a() { + return a(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the A button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the A button's digital signal attached + * to the given loop. + */ + public Trigger a(EventLoop loop) { + return button(StadiaController.Button.kA.value, loop); + } + + /** + * Constructs a Trigger instance around the B button's digital signal. + * + * @return a Trigger instance representing the B button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #b(EventLoop) + */ + public Trigger b() { + return b(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the B button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the B button's digital signal attached + * to the given loop. + */ + public Trigger b(EventLoop loop) { + return button(StadiaController.Button.kB.value, loop); + } + + /** + * Constructs a Trigger instance around the X button's digital signal. + * + * @return a Trigger instance representing the X button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #x(EventLoop) + */ + public Trigger x() { + return x(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the X button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the X button's digital signal attached + * to the given loop. + */ + public Trigger x(EventLoop loop) { + return button(StadiaController.Button.kX.value, loop); + } + + /** + * Constructs a Trigger instance around the Y button's digital signal. + * + * @return a Trigger instance representing the Y button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #y(EventLoop) + */ + public Trigger y() { + return y(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the Y button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the Y button's digital signal attached + * to the given loop. + */ + public Trigger y(EventLoop loop) { + return button(StadiaController.Button.kY.value, loop); + } + + /** + * Constructs a Trigger instance around the left bumper button's digital signal. + * + * @return a Trigger instance representing the left bumper button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #leftBumper(EventLoop) + */ + public Trigger leftBumper() { + return leftBumper(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left bumper button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left bumper button's digital signal attached + * to the given loop. + */ + public Trigger leftBumper(EventLoop loop) { + return button(StadiaController.Button.kLeftBumper.value, loop); + } + + /** + * Constructs a Trigger instance around the right bumper button's digital signal. + * + * @return a Trigger instance representing the right bumper button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #rightBumper(EventLoop) + */ + public Trigger rightBumper() { + return rightBumper(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right bumper button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right bumper button's digital signal attached + * to the given loop. + */ + public Trigger rightBumper(EventLoop loop) { + return button(StadiaController.Button.kRightBumper.value, loop); + } + + /** + * Constructs a Trigger instance around the left stick button's digital signal. + * + * @return a Trigger instance representing the left stick button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #leftStick(EventLoop) + */ + public Trigger leftStick() { + return leftStick(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left stick button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left stick button's digital signal attached + * to the given loop. + */ + public Trigger leftStick(EventLoop loop) { + return button(StadiaController.Button.kLeftStick.value, loop); + } + + /** + * Constructs a Trigger instance around the right stick button's digital signal. + * + * @return a Trigger instance representing the right stick button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #rightStick(EventLoop) + */ + public Trigger rightStick() { + return rightStick(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right stick button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right stick button's digital signal attached + * to the given loop. + */ + public Trigger rightStick(EventLoop loop) { + return button(StadiaController.Button.kRightStick.value, loop); + } + + /** + * Constructs a Trigger instance around the ellipses button's digital signal. + * + * @return a Trigger instance representing the ellipses button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #ellipses(EventLoop) + */ + public Trigger ellipses() { + return ellipses(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the ellipses button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the ellipses button's digital signal attached + * to the given loop. + */ + public Trigger ellipses(EventLoop loop) { + return button(StadiaController.Button.kEllipses.value, loop); + } + + /** + * Constructs a Trigger instance around the hamburger button's digital signal. + * + * @return a Trigger instance representing the hamburger button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #hamburger(EventLoop) + */ + public Trigger hamburger() { + return hamburger(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the hamburger button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the hamburger button's digital signal attached + * to the given loop. + */ + public Trigger hamburger(EventLoop loop) { + return button(StadiaController.Button.kHamburger.value, loop); + } + + /** + * Constructs a Trigger instance around the stadia button's digital signal. + * + * @return a Trigger instance representing the stadia button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #stadia(EventLoop) + */ + public Trigger stadia() { + return stadia(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the stadia button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the stadia button's digital signal attached + * to the given loop. + */ + public Trigger stadia(EventLoop loop) { + return button(StadiaController.Button.kStadia.value, loop); + } + + /** + * Constructs a Trigger instance around the right trigger button's digital signal. + * + * @return a Trigger instance representing the right trigger button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #rightTrigger(EventLoop) + */ + public Trigger rightTrigger() { + return rightTrigger(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right trigger button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right trigger button's digital signal attached + * to the given loop. + */ + public Trigger rightTrigger(EventLoop loop) { + return button(StadiaController.Button.kRightTrigger.value, loop); + } + + /** + * Constructs a Trigger instance around the left trigger button's digital signal. + * + * @return a Trigger instance representing the left trigger button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #leftTrigger(EventLoop) + */ + public Trigger leftTrigger() { + return leftTrigger(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left trigger button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left trigger button's digital signal attached + * to the given loop. + */ + public Trigger leftTrigger(EventLoop loop) { + return button(StadiaController.Button.kLeftTrigger.value, loop); + } + + /** + * Constructs a Trigger instance around the google button's digital signal. + * + * @return a Trigger instance representing the google button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #google(EventLoop) + */ + public Trigger google() { + return google(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the google button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the google button's digital signal attached + * to the given loop. + */ + public Trigger google(EventLoop loop) { + return button(StadiaController.Button.kGoogle.value, loop); + } + + /** + * Constructs a Trigger instance around the frame button's digital signal. + * + * @return a Trigger instance representing the frame button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #frame(EventLoop) + */ + public Trigger frame() { + return frame(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the frame button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the frame button's digital signal attached + * to the given loop. + */ + public Trigger frame(EventLoop loop) { + return button(StadiaController.Button.kFrame.value, loop); + } + + /** + * Get the X axis value of left side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getLeftX() { + return m_hid.getLeftX(); + } + + /** + * Get the X axis value of right side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getRightX() { + return m_hid.getRightX(); + } + + /** + * Get the Y axis value of left side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getLeftY() { + return m_hid.getLeftY(); + } + + /** + * Get the Y axis value of right side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getRightY() { + return m_hid.getRightY(); + } +} diff --git a/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandXboxController.java b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandXboxController.java new file mode 100644 index 0000000000..ef1008d396 --- /dev/null +++ b/commandsv3/src/generated/main/java/org/wpilib/commands3/button/CommandXboxController.java @@ -0,0 +1,435 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +// THIS FILE WAS AUTO-GENERATED BY ./commandsv3/generate_files.py. DO NOT MODIFY + +package org.wpilib.commands3.button; + +import edu.wpi.first.wpilibj.XboxController; +import edu.wpi.first.wpilibj.event.EventLoop; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.Trigger; + +/** + * A version of {@link XboxController} with {@link Trigger} factories for command-based. + * + * @see XboxController + */ +@SuppressWarnings("MethodName") +public class CommandXboxController extends CommandGenericHID { + private final XboxController m_hid; + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the {@link Scheduler#getDefault() default scheduler} using its default event loop. + * + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandXboxController(int port) { + super(port); + m_hid = new XboxController(port); + } + + /** + * Construct an instance of a controller. Commands bound to buttons on the controller will be + * scheduled on the given scheduler using its default event loop. + * + * @param scheduler The scheduler that should execute the triggered commands. + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandXboxController(Scheduler scheduler, int port) { + super(scheduler, port); + m_hid = new XboxController(port); + } + + /** + * Get the underlying GenericHID object. + * + * @return the wrapped GenericHID object + */ + @Override + public XboxController getHID() { + return m_hid; + } + + /** + * Constructs a Trigger instance around the A button's digital signal. + * + * @return a Trigger instance representing the A button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #a(EventLoop) + */ + public Trigger a() { + return a(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the A button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the A button's digital signal attached + * to the given loop. + */ + public Trigger a(EventLoop loop) { + return button(XboxController.Button.kA.value, loop); + } + + /** + * Constructs a Trigger instance around the B button's digital signal. + * + * @return a Trigger instance representing the B button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #b(EventLoop) + */ + public Trigger b() { + return b(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the B button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the B button's digital signal attached + * to the given loop. + */ + public Trigger b(EventLoop loop) { + return button(XboxController.Button.kB.value, loop); + } + + /** + * Constructs a Trigger instance around the X button's digital signal. + * + * @return a Trigger instance representing the X button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #x(EventLoop) + */ + public Trigger x() { + return x(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the X button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the X button's digital signal attached + * to the given loop. + */ + public Trigger x(EventLoop loop) { + return button(XboxController.Button.kX.value, loop); + } + + /** + * Constructs a Trigger instance around the Y button's digital signal. + * + * @return a Trigger instance representing the Y button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #y(EventLoop) + */ + public Trigger y() { + return y(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the Y button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the Y button's digital signal attached + * to the given loop. + */ + public Trigger y(EventLoop loop) { + return button(XboxController.Button.kY.value, loop); + } + + /** + * Constructs a Trigger instance around the left bumper button's digital signal. + * + * @return a Trigger instance representing the left bumper button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #leftBumper(EventLoop) + */ + public Trigger leftBumper() { + return leftBumper(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left bumper button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left bumper button's digital signal attached + * to the given loop. + */ + public Trigger leftBumper(EventLoop loop) { + return button(XboxController.Button.kLeftBumper.value, loop); + } + + /** + * Constructs a Trigger instance around the right bumper button's digital signal. + * + * @return a Trigger instance representing the right bumper button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #rightBumper(EventLoop) + */ + public Trigger rightBumper() { + return rightBumper(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right bumper button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right bumper button's digital signal attached + * to the given loop. + */ + public Trigger rightBumper(EventLoop loop) { + return button(XboxController.Button.kRightBumper.value, loop); + } + + /** + * Constructs a Trigger instance around the back button's digital signal. + * + * @return a Trigger instance representing the back button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #back(EventLoop) + */ + public Trigger back() { + return back(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the back button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the back button's digital signal attached + * to the given loop. + */ + public Trigger back(EventLoop loop) { + return button(XboxController.Button.kBack.value, loop); + } + + /** + * Constructs a Trigger instance around the start button's digital signal. + * + * @return a Trigger instance representing the start button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #start(EventLoop) + */ + public Trigger start() { + return start(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the start button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the start button's digital signal attached + * to the given loop. + */ + public Trigger start(EventLoop loop) { + return button(XboxController.Button.kStart.value, loop); + } + + /** + * Constructs a Trigger instance around the left stick button's digital signal. + * + * @return a Trigger instance representing the left stick button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #leftStick(EventLoop) + */ + public Trigger leftStick() { + return leftStick(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the left stick button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the left stick button's digital signal attached + * to the given loop. + */ + public Trigger leftStick(EventLoop loop) { + return button(XboxController.Button.kLeftStick.value, loop); + } + + /** + * Constructs a Trigger instance around the right stick button's digital signal. + * + * @return a Trigger instance representing the right stick button's digital signal attached + * to the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + * @see #rightStick(EventLoop) + */ + public Trigger rightStick() { + return rightStick(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the right stick button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return a Trigger instance representing the right stick button's digital signal attached + * to the given loop. + */ + public Trigger rightStick(EventLoop loop) { + return button(XboxController.Button.kRightStick.value, loop); + } + + /** + * Constructs a Trigger instance around the axis value of the left trigger. The returned + * trigger will be true when the axis value is greater than {@code threshold}. + * + * @param threshold the minimum axis value for the returned {@link Trigger} to be true. This value + * should be in the range [0, 1] where 0 is the unpressed state of the axis. + * @param loop the event loop instance to attach the Trigger to. + * @return a Trigger instance that is true when the left trigger's axis exceeds the provided + * threshold, attached to the given event loop + */ + public Trigger leftTrigger(double threshold, EventLoop loop) { + return axisGreaterThan(XboxController.Axis.kLeftTrigger.value, threshold, loop); + } + + /** + * Constructs a Trigger instance around the axis value of the left trigger. The returned + * trigger will be true when the axis value is greater than {@code threshold}. + * + * @param threshold the minimum axis value for the returned {@link Trigger} to be true. This value + * should be in the range [0, 1] where 0 is the unpressed state of the axis. + * @return a Trigger instance that is true when the left trigger's axis exceeds the provided + * threshold, attached to the {@link Scheduler#getDefaultEventLoop() default scheduler event + * loop} on the scheduler passed to the controller's constructor, or the {@link + * Scheduler#getDefault default scheduler} if a scheduler was not explicitly provided. + */ + public Trigger leftTrigger(double threshold) { + return leftTrigger(threshold, getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the axis value of the left trigger. The returned trigger + * will be true when the axis value is greater than 0.5. + * + * @return a Trigger instance that is true when the left trigger's axis exceeds 0.5, attached to + * the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + */ + public Trigger leftTrigger() { + return leftTrigger(0.5); + } + + /** + * Constructs a Trigger instance around the axis value of the right trigger. The returned + * trigger will be true when the axis value is greater than {@code threshold}. + * + * @param threshold the minimum axis value for the returned {@link Trigger} to be true. This value + * should be in the range [0, 1] where 0 is the unpressed state of the axis. + * @param loop the event loop instance to attach the Trigger to. + * @return a Trigger instance that is true when the right trigger's axis exceeds the provided + * threshold, attached to the given event loop + */ + public Trigger rightTrigger(double threshold, EventLoop loop) { + return axisGreaterThan(XboxController.Axis.kRightTrigger.value, threshold, loop); + } + + /** + * Constructs a Trigger instance around the axis value of the right trigger. The returned + * trigger will be true when the axis value is greater than {@code threshold}. + * + * @param threshold the minimum axis value for the returned {@link Trigger} to be true. This value + * should be in the range [0, 1] where 0 is the unpressed state of the axis. + * @return a Trigger instance that is true when the right trigger's axis exceeds the provided + * threshold, attached to the {@link Scheduler#getDefaultEventLoop() default scheduler event + * loop} on the scheduler passed to the controller's constructor, or the {@link + * Scheduler#getDefault default scheduler} if a scheduler was not explicitly provided. + */ + public Trigger rightTrigger(double threshold) { + return rightTrigger(threshold, getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance around the axis value of the right trigger. The returned trigger + * will be true when the axis value is greater than 0.5. + * + * @return a Trigger instance that is true when the right trigger's axis exceeds 0.5, attached to + * the {@link Scheduler#getDefaultEventLoop() default scheduler event loop} on the + * scheduler passed to the controller's constructor, or the {@link Scheduler#getDefault + * default scheduler} if a scheduler was not explicitly provided. + */ + public Trigger rightTrigger() { + return rightTrigger(0.5); + } + + /** + * Get the X axis value of left side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getLeftX() { + return m_hid.getLeftX(); + } + + /** + * Get the X axis value of right side of the controller. Right is positive. + * + * @return The axis value. + */ + public double getRightX() { + return m_hid.getRightX(); + } + + /** + * Get the Y axis value of left side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getLeftY() { + return m_hid.getLeftY(); + } + + /** + * Get the Y axis value of right side of the controller. Back is positive. + * + * @return The axis value. + */ + public double getRightY() { + return m_hid.getRightY(); + } + + /** + * Get the left trigger axis value of the controller. Note that this axis is bound to the + * range of [0, 1] as opposed to the usual [-1, 1]. + * + * @return The axis value. + */ + public double getLeftTriggerAxis() { + return m_hid.getLeftTriggerAxis(); + } + + /** + * Get the right trigger axis value of the controller. Note that this axis is bound to the + * range of [0, 1] as opposed to the usual [-1, 1]. + * + * @return The axis value. + */ + public double getRightTriggerAxis() { + return m_hid.getRightTriggerAxis(); + } +} diff --git a/commandsv3/src/generated/main/java/org/wpilib/commands3/proto/ProtobufCommands.java b/commandsv3/src/generated/main/java/org/wpilib/commands3/proto/ProtobufCommands.java new file mode 100644 index 0000000000..d0b91ff476 --- /dev/null +++ b/commandsv3/src/generated/main/java/org/wpilib/commands3/proto/ProtobufCommands.java @@ -0,0 +1,1883 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. +// Code generated by protocol buffer compiler. Do not edit! +package org.wpilib.commands3.proto; + +import java.io.IOException; +import us.hebi.quickbuf.Descriptors; +import us.hebi.quickbuf.FieldName; +import us.hebi.quickbuf.InvalidProtocolBufferException; +import us.hebi.quickbuf.JsonSink; +import us.hebi.quickbuf.JsonSource; +import us.hebi.quickbuf.MessageFactory; +import us.hebi.quickbuf.ProtoMessage; +import us.hebi.quickbuf.ProtoSink; +import us.hebi.quickbuf.ProtoSource; +import us.hebi.quickbuf.ProtoUtil; +import us.hebi.quickbuf.RepeatedByte; +import us.hebi.quickbuf.RepeatedMessage; +import us.hebi.quickbuf.Utf8String; + +public final class ProtobufCommands { + private static final RepeatedByte descriptorData = ProtoUtil.decodeBase64(2543, + "Chdwcm90b2J1Zl9jb21tYW5kcy5wcm90bxIJd3BpLnByb3RvIicKEVByb3RvYnVmTWVjaGFuaXNtEhIK" + + "BG5hbWUYASABKAlSBG5hbWUitgIKD1Byb3RvYnVmQ29tbWFuZBIOCgJpZBgBIAEoDVICaWQSIAoJcGFy" + + "ZW50X2lkGAIgASgNSABSCHBhcmVudElkiAEBEhIKBG5hbWUYAyABKAlSBG5hbWUSGgoIcHJpb3JpdHkY" + + "BCABKAVSCHByaW9yaXR5EkAKDHJlcXVpcmVtZW50cxgFIAMoCzIcLndwaS5wcm90by5Qcm90b2J1Zk1l" + + "Y2hhbmlzbVIMcmVxdWlyZW1lbnRzEiUKDGxhc3RfdGltZV9tcxgGIAEoAUgBUgpsYXN0VGltZU1ziAEB" + + "EicKDXRvdGFsX3RpbWVfbXMYByABKAFIAlILdG90YWxUaW1lTXOIAQFCDAoKX3BhcmVudF9pZEIPCg1f" + + "bGFzdF90aW1lX21zQhAKDl90b3RhbF90aW1lX21zIsEBChFQcm90b2J1ZlNjaGVkdWxlchJDCg9xdWV1" + + "ZWRfY29tbWFuZHMYASADKAsyGi53cGkucHJvdG8uUHJvdG9idWZDb21tYW5kUg5xdWV1ZWRDb21tYW5k" + + "cxJFChBydW5uaW5nX2NvbW1hbmRzGAIgAygLMhoud3BpLnByb3RvLlByb3RvYnVmQ29tbWFuZFIPcnVu" + + "bmluZ0NvbW1hbmRzEiAKDGxhc3RfdGltZV9tcxgDIAEoAVIKbGFzdFRpbWVNc0IcChpvcmcud3BpbGli" + + "LmNvbW1hbmRzMy5wcm90b0r8DgoGEgQAADgBCggKAQwSAwAAEgoICgECEgMCABIKCAoBCBIDBAAzCgkK" + + "AggBEgMEADMK8QEKAgQAEgQQABIBMuQBCk9yIHVzZSB0aGUgZ2VuZXJhdGVfZmlsZXMucHkgc2NyaXB0" + + "OgoKIyBtYWNPUwphbGx3cGlsaWIgJCAuL2NvbW1hbmRzdjMvZ2VuZXJhdGVfZmlsZXMucHkgLS1wcm90" + + "b2M9cHJvdG9jLXF1aWNrYnVmCgojIExpbnV4CmFsbHdwaWxpYiAkIC4vY29tbWFuZHN2My9nZW5lcmF0" + + "ZV9maWxlcy5weSAtLXF1aWNrYnVmX3BsdWdpbiBwcm90b2MtZ2VuLXF1aWNrYnVmLTEuMy4zLWxpbnV4" + + "LXg4Nl82NC5leGUKCgoKAwQAARIDEAgZCgsKBAQAAgASAxECEgoMCgUEAAIABRIDEQIICgwKBQQAAgAB" + + "EgMRCQ0KDAoFBAACAAMSAxEQEQoKCgIEARIEFAAtAQoKCgMEAQESAxQIFwpxCgQEAQIAEgMXAhAaZCBB" + + "IHVuaXF1ZSBJRCBmb3IgdGhlIGNvbW1hbmQuCiBEaWZmZXJlbnQgaW52b2NhdGlvbnMgb2YgdGhlIHNh" + + "bWUgY29tbWFuZCBvYmplY3QgaGF2ZSBkaWZmZXJlbnQgSURzLgoKDAoFBAECAAUSAxcCCAoMCgUEAQIA" + + "ARIDFwkLCgwKBQQBAgADEgMXDg8KYQoEBAECARIDGwIgGlQgVGhlIElEIG9mIHRoZSBwYXJlbnQgY29t", + "bWFuZC4KIE5vdCBpbmNsdWRlZCBpbiB0aGUgbWVzc2FnZSBmb3IgdG9wLWxldmVsIGNvbW1hbmRzLgoK" + + "DAoFBAECAQQSAxsCCgoMCgUEAQIBBRIDGwsRCgwKBQQBAgEBEgMbEhsKDAoFBAECAQMSAxseHwonCgQE" + + "AQICEgMeAhIaGiBUaGUgbmFtZSBvZiB0aGUgY29tbWFuZC4KCgwKBQQBAgIFEgMeAggKDAoFBAECAgES" + + "Ax4JDQoMCgUEAQICAxIDHhARCjEKBAQBAgMSAyECFRokIFRoZSBwcmlvcml0eSBsZXZlbCBvZiB0aGUg" + + "Y29tbWFuZC4KCgwKBQQBAgMFEgMhAgcKDAoFBAECAwESAyEIEAoMCgUEAQIDAxIDIRMUCjYKBAQBAgQS" + + "AyQCLhopIFRoZSBtZWNoYW5pc21zIHJlcXVpcmVkIGJ5IHRoZSBjb21tYW5kLgoKDAoFBAECBAQSAyQC" + + "CgoMCgUEAQIEBhIDJAscCgwKBQQBAgQBEgMkHSkKDAoFBAECBAMSAyQsLQqOAQoEBAECBRIDKAIjGoAB" + + "IEhvdyBtdWNoIHRpbWUgdGhlIGNvbW1hbmQgdG9vayB0byBleGVjdXRlIGluIGl0cyBtb3N0IHJlY2Vu" + + "dCBydW4uCiBPbmx5IGluY2x1ZGVkIGluIGEgbWVzc2FnZSBmb3IgYW4gYWN0aXZlbHkgcnVubmluZyBj" + + "b21tYW5kLgoKDAoFBAECBQQSAygCCgoMCgUEAQIFBRIDKAsRCgwKBQQBAgUBEgMoEh4KDAoFBAECBQMS" + + "AyghIgqAAQoEBAECBhIDLAIkGnMgSG93IGxvbmcgdGhlIGNvbW1hbmQgaGFzIHRha2VuIHRvIHJ1biwg" + + "aW4gYWdncmVnYXRlLgogT25seSBpbmNsdWRlZCBpbiBhIG1lc3NhZ2UgZm9yIGFuIGFjdGl2ZWx5IHJ1" + + "bm5pbmcgY29tbWFuZC4KCgwKBQQBAgYEEgMsAgoKDAoFBAECBgUSAywLEQoMCgUEAQIGARIDLBIfCgwK" + + "BQQBAgYDEgMsIiMKCgoCBAISBC8AOAEKCgoDBAIBEgMvCBkKjQIKBAQCAgASAzMCLxr/ASBOb3RlOiBj" + + "b21tYW5kcyBhcmUgZ2VuZXJhbGx5IHF1ZXVlZCBieSB0cmlnZ2Vycywgd2hpY2ggb2NjdXJzIGltbWVk" + + "aWF0ZWx5IGJlZm9yZSB0aGV5IGFyZQogcHJvbW90ZWQgYW5kIHN0YXJ0IHJ1bm5pbmcuIEVudHJpZXMg" + + "d2lsbCBvbmx5IGFwcGVhciBoZXJlIHdoZW4gc2VyaWFsaXppbmcgYSBzY2hlZHVsZXIKIF9hZnRlcl8g" + + "bWFudWFsbHkgc2NoZWR1bGluZyBhIGNvbW1hbmQgYnV0IF9iZWZvcmVfIGNhbGxpbmcgc2NoZWR1bGVy" + + "LnJ1bigpCgoMCgUEAgIABBIDMwIKCgwKBQQCAgAGEgMzCxoKDAoFBAICAAESAzMbKgoMCgUEAgIAAxID" + + "My0uCgsKBAQCAgESAzQCMAoMCgUEAgIBBBIDNAIKCgwKBQQCAgEGEgM0CxoKDAoFBAICAQESAzQbKwoM", + "CgUEAgIBAxIDNC4vCk8KBAQCAgISAzcCGhpCIEhvdyBtdWNoIHRpbWUgdGhlIHNjaGVkdWxlciB0b29r" + + "IGluIGl0cyBsYXN0IGBydW4oKWAgaW52b2NhdGlvbi4KCgwKBQQCAgIFEgM3AggKDAoFBAICAgESAzcJ" + + "FQoMCgUEAgICAxIDNxgZYgZwcm90bzM="); + + static final Descriptors.FileDescriptor descriptor = Descriptors.FileDescriptor.internalBuildGeneratedFileFrom("protobuf_commands.proto", "wpi.proto", descriptorData); + + static final Descriptors.Descriptor wpi_proto_ProtobufMechanism_descriptor = descriptor.internalContainedType(38, 39, "ProtobufMechanism", "wpi.proto.ProtobufMechanism"); + + static final Descriptors.Descriptor wpi_proto_ProtobufCommand_descriptor = descriptor.internalContainedType(80, 310, "ProtobufCommand", "wpi.proto.ProtobufCommand"); + + static final Descriptors.Descriptor wpi_proto_ProtobufScheduler_descriptor = descriptor.internalContainedType(393, 193, "ProtobufScheduler", "wpi.proto.ProtobufScheduler"); + + /** + * @return this proto file's descriptor. + */ + public static Descriptors.FileDescriptor getDescriptor() { + return descriptor; + } + + /** + * Protobuf type {@code ProtobufMechanism} + */ + public static final class ProtobufMechanism extends ProtoMessage implements Cloneable { + private static final long serialVersionUID = 0L; + + /** + * optional string name = 1; + */ + private final Utf8String name = Utf8String.newEmptyInstance(); + + private ProtobufMechanism() { + } + + /** + * @return a new empty instance of {@code ProtobufMechanism} + */ + public static ProtobufMechanism newInstance() { + return new ProtobufMechanism(); + } + + /** + * optional string name = 1; + * @return whether the name field is set + */ + public boolean hasName() { + return (bitField0_ & 0x00000001) != 0; + } + + /** + * optional string name = 1; + * @return this + */ + public ProtobufMechanism clearName() { + bitField0_ &= ~0x00000001; + name.clear(); + return this; + } + + /** + * optional string name = 1; + * @return the name + */ + public String getName() { + return name.getString(); + } + + /** + * optional string name = 1; + * @return internal {@code Utf8String} representation of name for reading + */ + public Utf8String getNameBytes() { + return this.name; + } + + /** + * optional string name = 1; + * @return internal {@code Utf8String} representation of name for modifications + */ + public Utf8String getMutableNameBytes() { + bitField0_ |= 0x00000001; + return this.name; + } + + /** + * optional string name = 1; + * @param value the name to set + * @return this + */ + public ProtobufMechanism setName(final CharSequence value) { + bitField0_ |= 0x00000001; + name.copyFrom(value); + return this; + } + + /** + * optional string name = 1; + * @param value the name to set + * @return this + */ + public ProtobufMechanism setName(final Utf8String value) { + bitField0_ |= 0x00000001; + name.copyFrom(value); + return this; + } + + @Override + public ProtobufMechanism copyFrom(final ProtobufMechanism other) { + cachedSize = other.cachedSize; + if ((bitField0_ | other.bitField0_) != 0) { + bitField0_ = other.bitField0_; + name.copyFrom(other.name); + } + return this; + } + + @Override + public ProtobufMechanism mergeFrom(final ProtobufMechanism other) { + if (other.isEmpty()) { + return this; + } + cachedSize = -1; + if (other.hasName()) { + getMutableNameBytes().copyFrom(other.name); + } + return this; + } + + @Override + public ProtobufMechanism clear() { + if (isEmpty()) { + return this; + } + cachedSize = -1; + bitField0_ = 0; + name.clear(); + return this; + } + + @Override + public ProtobufMechanism clearQuick() { + if (isEmpty()) { + return this; + } + cachedSize = -1; + bitField0_ = 0; + name.clear(); + return this; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ProtobufMechanism)) { + return false; + } + ProtobufMechanism other = (ProtobufMechanism) o; + return bitField0_ == other.bitField0_ + && (!hasName() || name.equals(other.name)); + } + + @Override + public void writeTo(final ProtoSink output) throws IOException { + if ((bitField0_ & 0x00000001) != 0) { + output.writeRawByte((byte) 10); + output.writeStringNoTag(name); + } + } + + @Override + protected int computeSerializedSize() { + int size = 0; + if ((bitField0_ & 0x00000001) != 0) { + size += 1 + ProtoSink.computeStringSizeNoTag(name); + } + return size; + } + + @Override + @SuppressWarnings("fallthrough") + public ProtobufMechanism mergeFrom(final ProtoSource input) throws IOException { + // Enabled Fall-Through Optimization (QuickBuffers) + int tag = input.readTag(); + while (true) { + switch (tag) { + case 10: { + // name + input.readString(name); + bitField0_ |= 0x00000001; + tag = input.readTag(); + if (tag != 0) { + break; + } + } + case 0: { + return this; + } + default: { + if (!input.skipField(tag)) { + return this; + } + tag = input.readTag(); + break; + } + } + } + } + + @Override + public void writeTo(final JsonSink output) throws IOException { + output.beginObject(); + if ((bitField0_ & 0x00000001) != 0) { + output.writeString(FieldNames.name, name); + } + output.endObject(); + } + + @Override + public ProtobufMechanism mergeFrom(final JsonSource input) throws IOException { + if (!input.beginObject()) { + return this; + } + while (!input.isAtEnd()) { + switch (input.readFieldHash()) { + case 3373707: { + if (input.isAtField(FieldNames.name)) { + if (!input.trySkipNullValue()) { + input.readString(name); + bitField0_ |= 0x00000001; + } + } else { + input.skipUnknownField(); + } + break; + } + default: { + input.skipUnknownField(); + break; + } + } + } + input.endObject(); + return this; + } + + @Override + public ProtobufMechanism clone() { + return new ProtobufMechanism().copyFrom(this); + } + + @Override + public boolean isEmpty() { + return ((bitField0_) == 0); + } + + public static ProtobufMechanism parseFrom(final byte[] data) throws + InvalidProtocolBufferException { + return ProtoMessage.mergeFrom(new ProtobufMechanism(), data).checkInitialized(); + } + + public static ProtobufMechanism parseFrom(final ProtoSource input) throws IOException { + return ProtoMessage.mergeFrom(new ProtobufMechanism(), input).checkInitialized(); + } + + public static ProtobufMechanism parseFrom(final JsonSource input) throws IOException { + return ProtoMessage.mergeFrom(new ProtobufMechanism(), input).checkInitialized(); + } + + /** + * @return factory for creating ProtobufMechanism messages + */ + public static MessageFactory getFactory() { + return ProtobufMechanismFactory.INSTANCE; + } + + /** + * @return this type's descriptor. + */ + public static Descriptors.Descriptor getDescriptor() { + return ProtobufCommands.wpi_proto_ProtobufMechanism_descriptor; + } + + private enum ProtobufMechanismFactory implements MessageFactory { + INSTANCE; + + @Override + public ProtobufMechanism create() { + return ProtobufMechanism.newInstance(); + } + } + + /** + * Contains name constants used for serializing JSON + */ + static class FieldNames { + static final FieldName name = FieldName.forField("name"); + } + } + + /** + * Protobuf type {@code ProtobufCommand} + */ + public static final class ProtobufCommand extends ProtoMessage implements Cloneable { + private static final long serialVersionUID = 0L; + + /** + *
+     *  How much time the command took to execute in its most recent run.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double last_time_ms = 6; + */ + private double lastTimeMs; + + /** + *
+     *  How long the command has taken to run, in aggregate.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double total_time_ms = 7; + */ + private double totalTimeMs; + + /** + *
+     *  The priority level of the command.
+     * 
+ * + * optional int32 priority = 4; + */ + private int priority; + + /** + *
+     *  A unique ID for the command.
+     *  Different invocations of the same command object have different IDs.
+     * 
+ * + * optional uint32 id = 1; + */ + private int id; + + /** + *
+     *  The ID of the parent command.
+     *  Not included in the message for top-level commands.
+     * 
+ * + * optional uint32 parent_id = 2; + */ + private int parentId; + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + */ + private final Utf8String name = Utf8String.newEmptyInstance(); + + /** + *
+     *  The mechanisms required by the command.
+     * 
+ * + * repeated .wpi.proto.ProtobufMechanism requirements = 5; + */ + private final RepeatedMessage requirements = RepeatedMessage.newEmptyInstance(ProtobufMechanism.getFactory()); + + private ProtobufCommand() { + } + + /** + * @return a new empty instance of {@code ProtobufCommand} + */ + public static ProtobufCommand newInstance() { + return new ProtobufCommand(); + } + + /** + *
+     *  How much time the command took to execute in its most recent run.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double last_time_ms = 6; + * @return whether the lastTimeMs field is set + */ + public boolean hasLastTimeMs() { + return (bitField0_ & 0x00000002) != 0; + } + + /** + *
+     *  How much time the command took to execute in its most recent run.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double last_time_ms = 6; + * @return this + */ + public ProtobufCommand clearLastTimeMs() { + bitField0_ &= ~0x00000002; + lastTimeMs = 0D; + return this; + } + + /** + *
+     *  How much time the command took to execute in its most recent run.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double last_time_ms = 6; + * @return the lastTimeMs + */ + public double getLastTimeMs() { + return lastTimeMs; + } + + /** + *
+     *  How much time the command took to execute in its most recent run.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double last_time_ms = 6; + * @param value the lastTimeMs to set + * @return this + */ + public ProtobufCommand setLastTimeMs(final double value) { + bitField0_ |= 0x00000002; + lastTimeMs = value; + return this; + } + + /** + *
+     *  How long the command has taken to run, in aggregate.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double total_time_ms = 7; + * @return whether the totalTimeMs field is set + */ + public boolean hasTotalTimeMs() { + return (bitField0_ & 0x00000001) != 0; + } + + /** + *
+     *  How long the command has taken to run, in aggregate.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double total_time_ms = 7; + * @return this + */ + public ProtobufCommand clearTotalTimeMs() { + bitField0_ &= ~0x00000001; + totalTimeMs = 0D; + return this; + } + + /** + *
+     *  How long the command has taken to run, in aggregate.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double total_time_ms = 7; + * @return the totalTimeMs + */ + public double getTotalTimeMs() { + return totalTimeMs; + } + + /** + *
+     *  How long the command has taken to run, in aggregate.
+     *  Only included in a message for an actively running command.
+     * 
+ * + * optional double total_time_ms = 7; + * @param value the totalTimeMs to set + * @return this + */ + public ProtobufCommand setTotalTimeMs(final double value) { + bitField0_ |= 0x00000001; + totalTimeMs = value; + return this; + } + + /** + *
+     *  The priority level of the command.
+     * 
+ * + * optional int32 priority = 4; + * @return whether the priority field is set + */ + public boolean hasPriority() { + return (bitField0_ & 0x00000008) != 0; + } + + /** + *
+     *  The priority level of the command.
+     * 
+ * + * optional int32 priority = 4; + * @return this + */ + public ProtobufCommand clearPriority() { + bitField0_ &= ~0x00000008; + priority = 0; + return this; + } + + /** + *
+     *  The priority level of the command.
+     * 
+ * + * optional int32 priority = 4; + * @return the priority + */ + public int getPriority() { + return priority; + } + + /** + *
+     *  The priority level of the command.
+     * 
+ * + * optional int32 priority = 4; + * @param value the priority to set + * @return this + */ + public ProtobufCommand setPriority(final int value) { + bitField0_ |= 0x00000008; + priority = value; + return this; + } + + /** + *
+     *  A unique ID for the command.
+     *  Different invocations of the same command object have different IDs.
+     * 
+ * + * optional uint32 id = 1; + * @return whether the id field is set + */ + public boolean hasId() { + return (bitField0_ & 0x00000010) != 0; + } + + /** + *
+     *  A unique ID for the command.
+     *  Different invocations of the same command object have different IDs.
+     * 
+ * + * optional uint32 id = 1; + * @return this + */ + public ProtobufCommand clearId() { + bitField0_ &= ~0x00000010; + id = 0; + return this; + } + + /** + *
+     *  A unique ID for the command.
+     *  Different invocations of the same command object have different IDs.
+     * 
+ * + * optional uint32 id = 1; + * @return the id + */ + public int getId() { + return id; + } + + /** + *
+     *  A unique ID for the command.
+     *  Different invocations of the same command object have different IDs.
+     * 
+ * + * optional uint32 id = 1; + * @param value the id to set + * @return this + */ + public ProtobufCommand setId(final int value) { + bitField0_ |= 0x00000010; + id = value; + return this; + } + + /** + *
+     *  The ID of the parent command.
+     *  Not included in the message for top-level commands.
+     * 
+ * + * optional uint32 parent_id = 2; + * @return whether the parentId field is set + */ + public boolean hasParentId() { + return (bitField0_ & 0x00000004) != 0; + } + + /** + *
+     *  The ID of the parent command.
+     *  Not included in the message for top-level commands.
+     * 
+ * + * optional uint32 parent_id = 2; + * @return this + */ + public ProtobufCommand clearParentId() { + bitField0_ &= ~0x00000004; + parentId = 0; + return this; + } + + /** + *
+     *  The ID of the parent command.
+     *  Not included in the message for top-level commands.
+     * 
+ * + * optional uint32 parent_id = 2; + * @return the parentId + */ + public int getParentId() { + return parentId; + } + + /** + *
+     *  The ID of the parent command.
+     *  Not included in the message for top-level commands.
+     * 
+ * + * optional uint32 parent_id = 2; + * @param value the parentId to set + * @return this + */ + public ProtobufCommand setParentId(final int value) { + bitField0_ |= 0x00000004; + parentId = value; + return this; + } + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + * @return whether the name field is set + */ + public boolean hasName() { + return (bitField0_ & 0x00000020) != 0; + } + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + * @return this + */ + public ProtobufCommand clearName() { + bitField0_ &= ~0x00000020; + name.clear(); + return this; + } + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + * @return the name + */ + public String getName() { + return name.getString(); + } + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + * @return internal {@code Utf8String} representation of name for reading + */ + public Utf8String getNameBytes() { + return this.name; + } + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + * @return internal {@code Utf8String} representation of name for modifications + */ + public Utf8String getMutableNameBytes() { + bitField0_ |= 0x00000020; + return this.name; + } + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + * @param value the name to set + * @return this + */ + public ProtobufCommand setName(final CharSequence value) { + bitField0_ |= 0x00000020; + name.copyFrom(value); + return this; + } + + /** + *
+     *  The name of the command.
+     * 
+ * + * optional string name = 3; + * @param value the name to set + * @return this + */ + public ProtobufCommand setName(final Utf8String value) { + bitField0_ |= 0x00000020; + name.copyFrom(value); + return this; + } + + /** + *
+     *  The mechanisms required by the command.
+     * 
+ * + * repeated .wpi.proto.ProtobufMechanism requirements = 5; + * @return whether the requirements field is set + */ + public boolean hasRequirements() { + return (bitField0_ & 0x00000040) != 0; + } + + /** + *
+     *  The mechanisms required by the command.
+     * 
+ * + * repeated .wpi.proto.ProtobufMechanism requirements = 5; + * @return this + */ + public ProtobufCommand clearRequirements() { + bitField0_ &= ~0x00000040; + requirements.clear(); + return this; + } + + /** + *
+     *  The mechanisms required by the command.
+     * 
+ * + * repeated .wpi.proto.ProtobufMechanism requirements = 5; + * + * This method returns the internal storage object without modifying any has state. + * The returned object should not be modified and be treated as read-only. + * + * Use {@link #getMutableRequirements()} if you want to modify it. + * + * @return internal storage object for reading + */ + public RepeatedMessage getRequirements() { + return requirements; + } + + /** + *
+     *  The mechanisms required by the command.
+     * 
+ * + * repeated .wpi.proto.ProtobufMechanism requirements = 5; + * + * This method returns the internal storage object and sets the corresponding + * has state. The returned object will become part of this message and its + * contents may be modified as long as the has state is not cleared. + * + * @return internal storage object for modifications + */ + public RepeatedMessage getMutableRequirements() { + bitField0_ |= 0x00000040; + return requirements; + } + + /** + *
+     *  The mechanisms required by the command.
+     * 
+ * + * repeated .wpi.proto.ProtobufMechanism requirements = 5; + * @param value the requirements to add + * @return this + */ + public ProtobufCommand addRequirements(final ProtobufMechanism value) { + bitField0_ |= 0x00000040; + requirements.add(value); + return this; + } + + /** + *
+     *  The mechanisms required by the command.
+     * 
+ * + * repeated .wpi.proto.ProtobufMechanism requirements = 5; + * @param values the requirements to add + * @return this + */ + public ProtobufCommand addAllRequirements(final ProtobufMechanism... values) { + bitField0_ |= 0x00000040; + requirements.addAll(values); + return this; + } + + @Override + public ProtobufCommand copyFrom(final ProtobufCommand other) { + cachedSize = other.cachedSize; + if ((bitField0_ | other.bitField0_) != 0) { + bitField0_ = other.bitField0_; + lastTimeMs = other.lastTimeMs; + totalTimeMs = other.totalTimeMs; + priority = other.priority; + id = other.id; + parentId = other.parentId; + name.copyFrom(other.name); + requirements.copyFrom(other.requirements); + } + return this; + } + + @Override + public ProtobufCommand mergeFrom(final ProtobufCommand other) { + if (other.isEmpty()) { + return this; + } + cachedSize = -1; + if (other.hasLastTimeMs()) { + setLastTimeMs(other.lastTimeMs); + } + if (other.hasTotalTimeMs()) { + setTotalTimeMs(other.totalTimeMs); + } + if (other.hasPriority()) { + setPriority(other.priority); + } + if (other.hasId()) { + setId(other.id); + } + if (other.hasParentId()) { + setParentId(other.parentId); + } + if (other.hasName()) { + getMutableNameBytes().copyFrom(other.name); + } + if (other.hasRequirements()) { + getMutableRequirements().addAll(other.requirements); + } + return this; + } + + @Override + public ProtobufCommand clear() { + if (isEmpty()) { + return this; + } + cachedSize = -1; + bitField0_ = 0; + lastTimeMs = 0D; + totalTimeMs = 0D; + priority = 0; + id = 0; + parentId = 0; + name.clear(); + requirements.clear(); + return this; + } + + @Override + public ProtobufCommand clearQuick() { + if (isEmpty()) { + return this; + } + cachedSize = -1; + bitField0_ = 0; + name.clear(); + requirements.clearQuick(); + return this; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ProtobufCommand)) { + return false; + } + ProtobufCommand other = (ProtobufCommand) o; + return bitField0_ == other.bitField0_ + && (!hasLastTimeMs() || ProtoUtil.isEqual(lastTimeMs, other.lastTimeMs)) + && (!hasTotalTimeMs() || ProtoUtil.isEqual(totalTimeMs, other.totalTimeMs)) + && (!hasPriority() || priority == other.priority) + && (!hasId() || id == other.id) + && (!hasParentId() || parentId == other.parentId) + && (!hasName() || name.equals(other.name)) + && (!hasRequirements() || requirements.equals(other.requirements)); + } + + @Override + public void writeTo(final ProtoSink output) throws IOException { + if ((bitField0_ & 0x00000002) != 0) { + output.writeRawByte((byte) 49); + output.writeDoubleNoTag(lastTimeMs); + } + if ((bitField0_ & 0x00000001) != 0) { + output.writeRawByte((byte) 57); + output.writeDoubleNoTag(totalTimeMs); + } + if ((bitField0_ & 0x00000008) != 0) { + output.writeRawByte((byte) 32); + output.writeInt32NoTag(priority); + } + if ((bitField0_ & 0x00000010) != 0) { + output.writeRawByte((byte) 8); + output.writeUInt32NoTag(id); + } + if ((bitField0_ & 0x00000004) != 0) { + output.writeRawByte((byte) 16); + output.writeUInt32NoTag(parentId); + } + if ((bitField0_ & 0x00000020) != 0) { + output.writeRawByte((byte) 26); + output.writeStringNoTag(name); + } + if ((bitField0_ & 0x00000040) != 0) { + for (int i = 0; i < requirements.length(); i++) { + output.writeRawByte((byte) 42); + output.writeMessageNoTag(requirements.get(i)); + } + } + } + + @Override + protected int computeSerializedSize() { + int size = 0; + if ((bitField0_ & 0x00000002) != 0) { + size += 9; + } + if ((bitField0_ & 0x00000001) != 0) { + size += 9; + } + if ((bitField0_ & 0x00000008) != 0) { + size += 1 + ProtoSink.computeInt32SizeNoTag(priority); + } + if ((bitField0_ & 0x00000010) != 0) { + size += 1 + ProtoSink.computeUInt32SizeNoTag(id); + } + if ((bitField0_ & 0x00000004) != 0) { + size += 1 + ProtoSink.computeUInt32SizeNoTag(parentId); + } + if ((bitField0_ & 0x00000020) != 0) { + size += 1 + ProtoSink.computeStringSizeNoTag(name); + } + if ((bitField0_ & 0x00000040) != 0) { + size += (1 * requirements.length()) + ProtoSink.computeRepeatedMessageSizeNoTag(requirements); + } + return size; + } + + @Override + @SuppressWarnings("fallthrough") + public ProtobufCommand mergeFrom(final ProtoSource input) throws IOException { + // Enabled Fall-Through Optimization (QuickBuffers) + int tag = input.readTag(); + while (true) { + switch (tag) { + case 49: { + // lastTimeMs + lastTimeMs = input.readDouble(); + bitField0_ |= 0x00000002; + tag = input.readTag(); + if (tag != 57) { + break; + } + } + case 57: { + // totalTimeMs + totalTimeMs = input.readDouble(); + bitField0_ |= 0x00000001; + tag = input.readTag(); + if (tag != 32) { + break; + } + } + case 32: { + // priority + priority = input.readInt32(); + bitField0_ |= 0x00000008; + tag = input.readTag(); + if (tag != 8) { + break; + } + } + case 8: { + // id + id = input.readUInt32(); + bitField0_ |= 0x00000010; + tag = input.readTag(); + if (tag != 16) { + break; + } + } + case 16: { + // parentId + parentId = input.readUInt32(); + bitField0_ |= 0x00000004; + tag = input.readTag(); + if (tag != 26) { + break; + } + } + case 26: { + // name + input.readString(name); + bitField0_ |= 0x00000020; + tag = input.readTag(); + if (tag != 42) { + break; + } + } + case 42: { + // requirements + tag = input.readRepeatedMessage(requirements, tag); + bitField0_ |= 0x00000040; + if (tag != 0) { + break; + } + } + case 0: { + return this; + } + default: { + if (!input.skipField(tag)) { + return this; + } + tag = input.readTag(); + break; + } + } + } + } + + @Override + public void writeTo(final JsonSink output) throws IOException { + output.beginObject(); + if ((bitField0_ & 0x00000002) != 0) { + output.writeDouble(FieldNames.lastTimeMs, lastTimeMs); + } + if ((bitField0_ & 0x00000001) != 0) { + output.writeDouble(FieldNames.totalTimeMs, totalTimeMs); + } + if ((bitField0_ & 0x00000008) != 0) { + output.writeInt32(FieldNames.priority, priority); + } + if ((bitField0_ & 0x00000010) != 0) { + output.writeUInt32(FieldNames.id, id); + } + if ((bitField0_ & 0x00000004) != 0) { + output.writeUInt32(FieldNames.parentId, parentId); + } + if ((bitField0_ & 0x00000020) != 0) { + output.writeString(FieldNames.name, name); + } + if ((bitField0_ & 0x00000040) != 0) { + output.writeRepeatedMessage(FieldNames.requirements, requirements); + } + output.endObject(); + } + + @Override + public ProtobufCommand mergeFrom(final JsonSource input) throws IOException { + if (!input.beginObject()) { + return this; + } + while (!input.isAtEnd()) { + switch (input.readFieldHash()) { + case 1958056841: + case -740797521: { + if (input.isAtField(FieldNames.lastTimeMs)) { + if (!input.trySkipNullValue()) { + lastTimeMs = input.readDouble(); + bitField0_ |= 0x00000002; + } + } else { + input.skipUnknownField(); + } + break; + } + case -717217353: + case 1006112349: { + if (input.isAtField(FieldNames.totalTimeMs)) { + if (!input.trySkipNullValue()) { + totalTimeMs = input.readDouble(); + bitField0_ |= 0x00000001; + } + } else { + input.skipUnknownField(); + } + break; + } + case -1165461084: { + if (input.isAtField(FieldNames.priority)) { + if (!input.trySkipNullValue()) { + priority = input.readInt32(); + bitField0_ |= 0x00000008; + } + } else { + input.skipUnknownField(); + } + break; + } + case 3355: { + if (input.isAtField(FieldNames.id)) { + if (!input.trySkipNullValue()) { + id = input.readUInt32(); + bitField0_ |= 0x00000010; + } + } else { + input.skipUnknownField(); + } + break; + } + case 1175162725: + case 2070327504: { + if (input.isAtField(FieldNames.parentId)) { + if (!input.trySkipNullValue()) { + parentId = input.readUInt32(); + bitField0_ |= 0x00000004; + } + } else { + input.skipUnknownField(); + } + break; + } + case 3373707: { + if (input.isAtField(FieldNames.name)) { + if (!input.trySkipNullValue()) { + input.readString(name); + bitField0_ |= 0x00000020; + } + } else { + input.skipUnknownField(); + } + break; + } + case -1619874672: { + if (input.isAtField(FieldNames.requirements)) { + if (!input.trySkipNullValue()) { + input.readRepeatedMessage(requirements); + bitField0_ |= 0x00000040; + } + } else { + input.skipUnknownField(); + } + break; + } + default: { + input.skipUnknownField(); + break; + } + } + } + input.endObject(); + return this; + } + + @Override + public ProtobufCommand clone() { + return new ProtobufCommand().copyFrom(this); + } + + @Override + public boolean isEmpty() { + return ((bitField0_) == 0); + } + + public static ProtobufCommand parseFrom(final byte[] data) throws + InvalidProtocolBufferException { + return ProtoMessage.mergeFrom(new ProtobufCommand(), data).checkInitialized(); + } + + public static ProtobufCommand parseFrom(final ProtoSource input) throws IOException { + return ProtoMessage.mergeFrom(new ProtobufCommand(), input).checkInitialized(); + } + + public static ProtobufCommand parseFrom(final JsonSource input) throws IOException { + return ProtoMessage.mergeFrom(new ProtobufCommand(), input).checkInitialized(); + } + + /** + * @return factory for creating ProtobufCommand messages + */ + public static MessageFactory getFactory() { + return ProtobufCommandFactory.INSTANCE; + } + + /** + * @return this type's descriptor. + */ + public static Descriptors.Descriptor getDescriptor() { + return ProtobufCommands.wpi_proto_ProtobufCommand_descriptor; + } + + private enum ProtobufCommandFactory implements MessageFactory { + INSTANCE; + + @Override + public ProtobufCommand create() { + return ProtobufCommand.newInstance(); + } + } + + /** + * Contains name constants used for serializing JSON + */ + static class FieldNames { + static final FieldName lastTimeMs = FieldName.forField("lastTimeMs", "last_time_ms"); + + static final FieldName totalTimeMs = FieldName.forField("totalTimeMs", "total_time_ms"); + + static final FieldName priority = FieldName.forField("priority"); + + static final FieldName id = FieldName.forField("id"); + + static final FieldName parentId = FieldName.forField("parentId", "parent_id"); + + static final FieldName name = FieldName.forField("name"); + + static final FieldName requirements = FieldName.forField("requirements"); + } + } + + /** + * Protobuf type {@code ProtobufScheduler} + */ + public static final class ProtobufScheduler extends ProtoMessage implements Cloneable { + private static final long serialVersionUID = 0L; + + /** + *
+     *  How much time the scheduler took in its last `run()` invocation.
+     * 
+ * + * optional double last_time_ms = 3; + */ + private double lastTimeMs; + + /** + *
+     *  Note: commands are generally queued by triggers, which occurs immediately before they are
+     *  promoted and start running. Entries will only appear here when serializing a scheduler
+     *  _after_ manually scheduling a command but _before_ calling scheduler.run()
+     * 
+ * + * repeated .wpi.proto.ProtobufCommand queued_commands = 1; + */ + private final RepeatedMessage queuedCommands = RepeatedMessage.newEmptyInstance(ProtobufCommand.getFactory()); + + /** + * repeated .wpi.proto.ProtobufCommand running_commands = 2; + */ + private final RepeatedMessage runningCommands = RepeatedMessage.newEmptyInstance(ProtobufCommand.getFactory()); + + private ProtobufScheduler() { + } + + /** + * @return a new empty instance of {@code ProtobufScheduler} + */ + public static ProtobufScheduler newInstance() { + return new ProtobufScheduler(); + } + + /** + *
+     *  How much time the scheduler took in its last `run()` invocation.
+     * 
+ * + * optional double last_time_ms = 3; + * @return whether the lastTimeMs field is set + */ + public boolean hasLastTimeMs() { + return (bitField0_ & 0x00000001) != 0; + } + + /** + *
+     *  How much time the scheduler took in its last `run()` invocation.
+     * 
+ * + * optional double last_time_ms = 3; + * @return this + */ + public ProtobufScheduler clearLastTimeMs() { + bitField0_ &= ~0x00000001; + lastTimeMs = 0D; + return this; + } + + /** + *
+     *  How much time the scheduler took in its last `run()` invocation.
+     * 
+ * + * optional double last_time_ms = 3; + * @return the lastTimeMs + */ + public double getLastTimeMs() { + return lastTimeMs; + } + + /** + *
+     *  How much time the scheduler took in its last `run()` invocation.
+     * 
+ * + * optional double last_time_ms = 3; + * @param value the lastTimeMs to set + * @return this + */ + public ProtobufScheduler setLastTimeMs(final double value) { + bitField0_ |= 0x00000001; + lastTimeMs = value; + return this; + } + + /** + *
+     *  Note: commands are generally queued by triggers, which occurs immediately before they are
+     *  promoted and start running. Entries will only appear here when serializing a scheduler
+     *  _after_ manually scheduling a command but _before_ calling scheduler.run()
+     * 
+ * + * repeated .wpi.proto.ProtobufCommand queued_commands = 1; + * @return whether the queuedCommands field is set + */ + public boolean hasQueuedCommands() { + return (bitField0_ & 0x00000002) != 0; + } + + /** + *
+     *  Note: commands are generally queued by triggers, which occurs immediately before they are
+     *  promoted and start running. Entries will only appear here when serializing a scheduler
+     *  _after_ manually scheduling a command but _before_ calling scheduler.run()
+     * 
+ * + * repeated .wpi.proto.ProtobufCommand queued_commands = 1; + * @return this + */ + public ProtobufScheduler clearQueuedCommands() { + bitField0_ &= ~0x00000002; + queuedCommands.clear(); + return this; + } + + /** + *
+     *  Note: commands are generally queued by triggers, which occurs immediately before they are
+     *  promoted and start running. Entries will only appear here when serializing a scheduler
+     *  _after_ manually scheduling a command but _before_ calling scheduler.run()
+     * 
+ * + * repeated .wpi.proto.ProtobufCommand queued_commands = 1; + * + * This method returns the internal storage object without modifying any has state. + * The returned object should not be modified and be treated as read-only. + * + * Use {@link #getMutableQueuedCommands()} if you want to modify it. + * + * @return internal storage object for reading + */ + public RepeatedMessage getQueuedCommands() { + return queuedCommands; + } + + /** + *
+     *  Note: commands are generally queued by triggers, which occurs immediately before they are
+     *  promoted and start running. Entries will only appear here when serializing a scheduler
+     *  _after_ manually scheduling a command but _before_ calling scheduler.run()
+     * 
+ * + * repeated .wpi.proto.ProtobufCommand queued_commands = 1; + * + * This method returns the internal storage object and sets the corresponding + * has state. The returned object will become part of this message and its + * contents may be modified as long as the has state is not cleared. + * + * @return internal storage object for modifications + */ + public RepeatedMessage getMutableQueuedCommands() { + bitField0_ |= 0x00000002; + return queuedCommands; + } + + /** + *
+     *  Note: commands are generally queued by triggers, which occurs immediately before they are
+     *  promoted and start running. Entries will only appear here when serializing a scheduler
+     *  _after_ manually scheduling a command but _before_ calling scheduler.run()
+     * 
+ * + * repeated .wpi.proto.ProtobufCommand queued_commands = 1; + * @param value the queuedCommands to add + * @return this + */ + public ProtobufScheduler addQueuedCommands(final ProtobufCommand value) { + bitField0_ |= 0x00000002; + queuedCommands.add(value); + return this; + } + + /** + *
+     *  Note: commands are generally queued by triggers, which occurs immediately before they are
+     *  promoted and start running. Entries will only appear here when serializing a scheduler
+     *  _after_ manually scheduling a command but _before_ calling scheduler.run()
+     * 
+ * + * repeated .wpi.proto.ProtobufCommand queued_commands = 1; + * @param values the queuedCommands to add + * @return this + */ + public ProtobufScheduler addAllQueuedCommands(final ProtobufCommand... values) { + bitField0_ |= 0x00000002; + queuedCommands.addAll(values); + return this; + } + + /** + * repeated .wpi.proto.ProtobufCommand running_commands = 2; + * @return whether the runningCommands field is set + */ + public boolean hasRunningCommands() { + return (bitField0_ & 0x00000004) != 0; + } + + /** + * repeated .wpi.proto.ProtobufCommand running_commands = 2; + * @return this + */ + public ProtobufScheduler clearRunningCommands() { + bitField0_ &= ~0x00000004; + runningCommands.clear(); + return this; + } + + /** + * repeated .wpi.proto.ProtobufCommand running_commands = 2; + * + * This method returns the internal storage object without modifying any has state. + * The returned object should not be modified and be treated as read-only. + * + * Use {@link #getMutableRunningCommands()} if you want to modify it. + * + * @return internal storage object for reading + */ + public RepeatedMessage getRunningCommands() { + return runningCommands; + } + + /** + * repeated .wpi.proto.ProtobufCommand running_commands = 2; + * + * This method returns the internal storage object and sets the corresponding + * has state. The returned object will become part of this message and its + * contents may be modified as long as the has state is not cleared. + * + * @return internal storage object for modifications + */ + public RepeatedMessage getMutableRunningCommands() { + bitField0_ |= 0x00000004; + return runningCommands; + } + + /** + * repeated .wpi.proto.ProtobufCommand running_commands = 2; + * @param value the runningCommands to add + * @return this + */ + public ProtobufScheduler addRunningCommands(final ProtobufCommand value) { + bitField0_ |= 0x00000004; + runningCommands.add(value); + return this; + } + + /** + * repeated .wpi.proto.ProtobufCommand running_commands = 2; + * @param values the runningCommands to add + * @return this + */ + public ProtobufScheduler addAllRunningCommands(final ProtobufCommand... values) { + bitField0_ |= 0x00000004; + runningCommands.addAll(values); + return this; + } + + @Override + public ProtobufScheduler copyFrom(final ProtobufScheduler other) { + cachedSize = other.cachedSize; + if ((bitField0_ | other.bitField0_) != 0) { + bitField0_ = other.bitField0_; + lastTimeMs = other.lastTimeMs; + queuedCommands.copyFrom(other.queuedCommands); + runningCommands.copyFrom(other.runningCommands); + } + return this; + } + + @Override + public ProtobufScheduler mergeFrom(final ProtobufScheduler other) { + if (other.isEmpty()) { + return this; + } + cachedSize = -1; + if (other.hasLastTimeMs()) { + setLastTimeMs(other.lastTimeMs); + } + if (other.hasQueuedCommands()) { + getMutableQueuedCommands().addAll(other.queuedCommands); + } + if (other.hasRunningCommands()) { + getMutableRunningCommands().addAll(other.runningCommands); + } + return this; + } + + @Override + public ProtobufScheduler clear() { + if (isEmpty()) { + return this; + } + cachedSize = -1; + bitField0_ = 0; + lastTimeMs = 0D; + queuedCommands.clear(); + runningCommands.clear(); + return this; + } + + @Override + public ProtobufScheduler clearQuick() { + if (isEmpty()) { + return this; + } + cachedSize = -1; + bitField0_ = 0; + queuedCommands.clearQuick(); + runningCommands.clearQuick(); + return this; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ProtobufScheduler)) { + return false; + } + ProtobufScheduler other = (ProtobufScheduler) o; + return bitField0_ == other.bitField0_ + && (!hasLastTimeMs() || ProtoUtil.isEqual(lastTimeMs, other.lastTimeMs)) + && (!hasQueuedCommands() || queuedCommands.equals(other.queuedCommands)) + && (!hasRunningCommands() || runningCommands.equals(other.runningCommands)); + } + + @Override + public void writeTo(final ProtoSink output) throws IOException { + if ((bitField0_ & 0x00000001) != 0) { + output.writeRawByte((byte) 25); + output.writeDoubleNoTag(lastTimeMs); + } + if ((bitField0_ & 0x00000002) != 0) { + for (int i = 0; i < queuedCommands.length(); i++) { + output.writeRawByte((byte) 10); + output.writeMessageNoTag(queuedCommands.get(i)); + } + } + if ((bitField0_ & 0x00000004) != 0) { + for (int i = 0; i < runningCommands.length(); i++) { + output.writeRawByte((byte) 18); + output.writeMessageNoTag(runningCommands.get(i)); + } + } + } + + @Override + protected int computeSerializedSize() { + int size = 0; + if ((bitField0_ & 0x00000001) != 0) { + size += 9; + } + if ((bitField0_ & 0x00000002) != 0) { + size += (1 * queuedCommands.length()) + ProtoSink.computeRepeatedMessageSizeNoTag(queuedCommands); + } + if ((bitField0_ & 0x00000004) != 0) { + size += (1 * runningCommands.length()) + ProtoSink.computeRepeatedMessageSizeNoTag(runningCommands); + } + return size; + } + + @Override + @SuppressWarnings("fallthrough") + public ProtobufScheduler mergeFrom(final ProtoSource input) throws IOException { + // Enabled Fall-Through Optimization (QuickBuffers) + int tag = input.readTag(); + while (true) { + switch (tag) { + case 25: { + // lastTimeMs + lastTimeMs = input.readDouble(); + bitField0_ |= 0x00000001; + tag = input.readTag(); + if (tag != 10) { + break; + } + } + case 10: { + // queuedCommands + tag = input.readRepeatedMessage(queuedCommands, tag); + bitField0_ |= 0x00000002; + if (tag != 18) { + break; + } + } + case 18: { + // runningCommands + tag = input.readRepeatedMessage(runningCommands, tag); + bitField0_ |= 0x00000004; + if (tag != 0) { + break; + } + } + case 0: { + return this; + } + default: { + if (!input.skipField(tag)) { + return this; + } + tag = input.readTag(); + break; + } + } + } + } + + @Override + public void writeTo(final JsonSink output) throws IOException { + output.beginObject(); + if ((bitField0_ & 0x00000001) != 0) { + output.writeDouble(FieldNames.lastTimeMs, lastTimeMs); + } + if ((bitField0_ & 0x00000002) != 0) { + output.writeRepeatedMessage(FieldNames.queuedCommands, queuedCommands); + } + if ((bitField0_ & 0x00000004) != 0) { + output.writeRepeatedMessage(FieldNames.runningCommands, runningCommands); + } + output.endObject(); + } + + @Override + public ProtobufScheduler mergeFrom(final JsonSource input) throws IOException { + if (!input.beginObject()) { + return this; + } + while (!input.isAtEnd()) { + switch (input.readFieldHash()) { + case 1958056841: + case -740797521: { + if (input.isAtField(FieldNames.lastTimeMs)) { + if (!input.trySkipNullValue()) { + lastTimeMs = input.readDouble(); + bitField0_ |= 0x00000001; + } + } else { + input.skipUnknownField(); + } + break; + } + case -167160549: + case -1904270380: { + if (input.isAtField(FieldNames.queuedCommands)) { + if (!input.trySkipNullValue()) { + input.readRepeatedMessage(queuedCommands); + bitField0_ |= 0x00000002; + } + } else { + input.skipUnknownField(); + } + break; + } + case -1719052953: + case 1526672648: { + if (input.isAtField(FieldNames.runningCommands)) { + if (!input.trySkipNullValue()) { + input.readRepeatedMessage(runningCommands); + bitField0_ |= 0x00000004; + } + } else { + input.skipUnknownField(); + } + break; + } + default: { + input.skipUnknownField(); + break; + } + } + } + input.endObject(); + return this; + } + + @Override + public ProtobufScheduler clone() { + return new ProtobufScheduler().copyFrom(this); + } + + @Override + public boolean isEmpty() { + return ((bitField0_) == 0); + } + + public static ProtobufScheduler parseFrom(final byte[] data) throws + InvalidProtocolBufferException { + return ProtoMessage.mergeFrom(new ProtobufScheduler(), data).checkInitialized(); + } + + public static ProtobufScheduler parseFrom(final ProtoSource input) throws IOException { + return ProtoMessage.mergeFrom(new ProtobufScheduler(), input).checkInitialized(); + } + + public static ProtobufScheduler parseFrom(final JsonSource input) throws IOException { + return ProtoMessage.mergeFrom(new ProtobufScheduler(), input).checkInitialized(); + } + + /** + * @return factory for creating ProtobufScheduler messages + */ + public static MessageFactory getFactory() { + return ProtobufSchedulerFactory.INSTANCE; + } + + /** + * @return this type's descriptor. + */ + public static Descriptors.Descriptor getDescriptor() { + return ProtobufCommands.wpi_proto_ProtobufScheduler_descriptor; + } + + private enum ProtobufSchedulerFactory implements MessageFactory { + INSTANCE; + + @Override + public ProtobufScheduler create() { + return ProtobufScheduler.newInstance(); + } + } + + /** + * Contains name constants used for serializing JSON + */ + static class FieldNames { + static final FieldName lastTimeMs = FieldName.forField("lastTimeMs", "last_time_ms"); + + static final FieldName queuedCommands = FieldName.forField("queuedCommands", "queued_commands"); + + static final FieldName runningCommands = FieldName.forField("runningCommands", "running_commands"); + } + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/Binding.java b/commandsv3/src/main/java/org/wpilib/commands3/Binding.java new file mode 100644 index 0000000000..69943c22c5 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/Binding.java @@ -0,0 +1,26 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import edu.wpi.first.util.ErrorMessages; + +/** + * A single trigger binding. + * + * @param scope The scope in which the binding is active. + * @param type The type of binding; or, when the bound command should run + * @param command The bound command. Cannot be null. + * @param frames The stack frames when the binding was created. Used for telemetry and error + * reporting so if a command throws an exception, we can tell users where that command was bound + * instead of giving a fairly useless backtrace of the command framework. + */ +record Binding(BindingScope scope, BindingType type, Command command, StackTraceElement[] frames) { + public Binding { + ErrorMessages.requireNonNullParam(scope, "scope", "Binding"); + ErrorMessages.requireNonNullParam(type, "type", "Binding"); + ErrorMessages.requireNonNullParam(command, "command", "Binding"); + ErrorMessages.requireNonNullParam(frames, "frames", "Binding"); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/BindingScope.java b/commandsv3/src/main/java/org/wpilib/commands3/BindingScope.java new file mode 100644 index 0000000000..3925aca807 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/BindingScope.java @@ -0,0 +1,52 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +/** + * A scope for when a binding is live. Bindings tied to a scope must be deleted when the scope + * becomes inactive. + */ +@SuppressWarnings("PMD.ImplicitFunctionalInterface") +interface BindingScope { + /** + * Checks if the scope is active. Bindings for inactive scopes are removed from the scheduler. + * + * @return True if the scope is still active, false if not. + */ + boolean active(); + + static BindingScope global() { + return Global.INSTANCE; + } + + static BindingScope forCommand(Scheduler scheduler, Command command) { + return new ForCommand(scheduler, command); + } + + /** A global binding scope. Bindings in this scope are always active. */ + final class Global implements BindingScope { + // No reason not to be a singleton. + public static final Global INSTANCE = new Global(); + + @Override + public boolean active() { + return true; + } + } + + /** + * A binding scoped to the lifetime of a specific command. This should be used when a binding is + * created within a command, tying the lifetime of the binding to the declaring command. + * + * @param scheduler The scheduler managing the command. + * @param command The command being scoped to. + */ + record ForCommand(Scheduler scheduler, Command command) implements BindingScope { + @Override + public boolean active() { + return scheduler.isRunning(command); + } + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/BindingType.java b/commandsv3/src/main/java/org/wpilib/commands3/BindingType.java new file mode 100644 index 0000000000..188c39acb8 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/BindingType.java @@ -0,0 +1,46 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +/** Describes when a command bound to a trigger should run. */ +enum BindingType { + /** + * An immediate or manual binding created when calling {@link Scheduler#schedule(Command)} + * directly, without using a trigger to bind it to a signal. + */ + IMMEDIATE, + /** + * Schedules (forks) a command on a rising edge signal. The command will run until it completes or + * is interrupted by another command requiring the same mechanisms. + */ + SCHEDULE_ON_RISING_EDGE, + /** + * Schedules (forks) a command on a falling edge signal. The command will run until it completes + * or is interrupted by another command requiring the same mechanisms. + */ + SCHEDULE_ON_FALLING_EDGE, + /** + * Schedules (forks) a command on a rising edge signal. If the command is still running on the + * next rising edge, it will be canceled then; otherwise, it will be scheduled again. + */ + TOGGLE_ON_RISING_EDGE, + /** + * Schedules (forks) a command on a falling edge signal. If the command is still running on the + * next falling edge, it will be canceled then; otherwise, it will be scheduled again. + */ + TOGGLE_ON_FALLING_EDGE, + /** + * Schedules a command on a rising edge signal. If the command is still running on the next + * falling edge, it will be canceled then - unlike {@link #SCHEDULE_ON_RISING_EDGE}, which would + * allow it to continue to run. + */ + RUN_WHILE_HIGH, + /** + * Schedules a command on a falling edge signal. If the command is still running on the next + * rising edge, it will be canceled then - unlike {@link #SCHEDULE_ON_FALLING_EDGE}, which would + * allow it to continue to run. + */ + RUN_WHILE_LOW +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/Command.java b/commandsv3/src/main/java/org/wpilib/commands3/Command.java new file mode 100644 index 0000000000..4e75a6918b --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/Command.java @@ -0,0 +1,416 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import edu.wpi.first.units.measure.Time; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import org.wpilib.annotation.NoDiscard; + +/** + * Performs some task using one or more {@link Mechanism mechanisms}. Commands are fundamentally + * backed by a {@link Coroutine} that can be used to write imperative code that runs asynchronously. + * + *

Programmers familiar with the earlier versions of the command framework can think of a v3 + * command similar to a v1 or v2 command that executes the lifecycle methods in a single method, as + * demonstrated in the example below. (Note, however, that more sophisticated code than this is + * possible! + * + *

{@code
+ * coroutine -> {
+ *   initialize();
+ *   while (!isFinished()) {
+ *     execute();
+ *     coroutine.yield(); // be sure to yield at the end of the loop
+ *   }
+ *   end();
+ * }
+ * }
+ * + *

Note: Because coroutines are opt-in collaborate constructs, every + * command implementation must call {@link Coroutine#yield()} within any periodic + * loop. Failure to do so may result in an unrecoverable infinite loop. + * + *

Requirements

+ * + *

Commands require zero or more mechanisms. To prevent conflicting control requests from running + * simultaneously (for example, commanding an elevator to both raise and lower at the same time), a + * running command has exclusive ownership of all of its required mechanisms. If another + * command with an equal or greater {@link #priority()} is scheduled that requires one or more of + * those same mechanisms, it will interrupt and cancel the running command. + * + *

The recommended way to create a command is using {@link Mechanism#run(Consumer)} or a related + * factory method to create commands that require a single mechanism (for example, a command that + * drives an elevator up and down or rotates an arm). Commands may be composed into {@link + * ParallelGroup parallel groups} and {@link SequentialGroup sequences} to build more complex + * behavior out of fundamental building blocks. These built-in compositions will require every + * mechanism used by every command in them, even if those commands aren't always running, and thus + * can leave certain required mechanisms in an uncommanded state: owned, but not used, this + * can lead to mechanisms sagging under gravity or running the previous motor control request they + * were given. + * + *

Advanced Usage

+ * + *

For example, a hypothetical drive-and-score sequence could be coded in two ways: one with a + * sequence chain, or one that uses the lower-level coroutine API. Coroutines can be used in an + * async/await style that you may be familiar with from languages like JavaScript, Python, or C# + * (note that there is no {@code async} keyword; commands are inherently asynchronous). Nested + * commands may be forked and awaited, but will not outlive the command that forked them; there is + * no analog for something like a {@code ScheduleCommand} from the v2 commands framework. + * + *

{@code
+ * class Robot extends TimedRobot {
+ *   private final Drivetrain drivetrain = new Drivetrain();
+ *   private final Elevator elevator = new Elevator();
+ *   private final Gripper gripper = new Gripper();
+ *
+ *  // Basic sequence builder - owns all 3 mechanisms for the full duration,
+ *  // even when they're not being used
+ *  private Command basicScoringSequence() {
+ *     return drivetrain.driveToScoringLocation()
+ *       .andThen(elevator.moveToScoringHeight())
+ *       .andThen(gripper.release())
+ *       .named("Scoring Sequence (Basic)");
+ *   }
+ *
+ *   // Advanced sequence with async/await - only owns mechanisms while they're
+ *   // being used, allowing default commands or other user-triggered commands
+ *   // to run when not in use. Interrupting one of the inner commands while it's
+ *   // running will cancel the entire sequence.
+ *   private Command advancedScoringSequence() {
+ *     return Command.noRequirements().executing(coroutine -> {
+ *       coroutine.await(drivetrain.driveToScoringLocation());
+ *       coroutine.await(elevator.moveToScoringHeight());
+ *       coroutine.await(gripper.release());
+ *     }).named("Scoring Sequence (Advanced)");
+ *   }
+ * }
+ * }
+ */ +@NoDiscard("Commands must be used! Did you mean to fork it or bind it to a trigger?") +public interface Command { + /** The default command priority. */ + int DEFAULT_PRIORITY = 0; + + /** + * The lowest possible command priority. Commands with the lowest priority can be interrupted by + * any other command, including other minimum-priority commands. + */ + int LOWEST_PRIORITY = Integer.MIN_VALUE; + + /** + * The highest possible command priority. Commands with the highest priority can only be + * interrupted by other maximum-priority commands. + */ + int HIGHEST_PRIORITY = Integer.MAX_VALUE; + + /** + * Runs the command. Commands that need to periodically run until a goal state is reached should + * simply run a while loop like {@code while (!atGoal()) { ... } } and call {@link + * Coroutine#yield()} at the end of the loop. + * + *

Warning: any loops in a command must call {@code coroutine.yield()}. + * Failure to do so will prevent anything else in your robot code from running. Commands are + * opt-in collaborative constructs; don't be greedy! + * + * @param coroutine the coroutine backing the command's execution + */ + void run(Coroutine coroutine); + + /** + * An optional lifecycle hook that can be implemented to execute some code whenever this command + * is canceled before it naturally completes. Commands should be careful to do a single-shot + * cleanup (for example, setting a motor to zero volts) and not do any complex looping logic here. + */ + default void onCancel() { + // NOP by default + } + + /** + * The name of the command. + * + * @return the name of the command + */ + @NoDiscard + String name(); + + /** + * The mechanisms required by the command. This is used by the scheduler to determine if two + * commands conflict with each other. No mechanism may be required by more than one running + * command at a time. + * + * @return the set of mechanisms required by the command + */ + @NoDiscard + Set requirements(); + + /** + * The priority of the command. If a command is scheduled that conflicts with another running or + * pending command, their priority values are compared to determine which should run. If the + * scheduled command is lower priority than the running command, then it will not be scheduled and + * the running command will continue to run. If it is the same or higher priority, then the + * running command will be canceled and the scheduled command will start to run. + * + * @return the priority of the command + */ + @NoDiscard + default int priority() { + return DEFAULT_PRIORITY; + } + + /** + * Checks if this command has a lower {@link #priority() priority} than another command. + * + * @param other the command to compare with + * @return true if this command has a lower priority than the other one, false otherwise + */ + @NoDiscard + default boolean isLowerPriorityThan(Command other) { + requireNonNullParam(other, "other", "Command.isLowerPriorityThan"); + + return priority() < other.priority(); + } + + /** + * Checks if this command requires a particular mechanism. + * + * @param mechanism the mechanism to check + * @return true if the mechanism is required, false if not + */ + @NoDiscard + default boolean requires(Mechanism mechanism) { + return requirements().contains(mechanism); + } + + /** + * Checks if this command conflicts with another command. + * + * @param other the commands to check against + * @return true if both commands require at least one of the same mechanism, false if both + * commands have completely different requirements + */ + @NoDiscard + default boolean conflictsWith(Command other) { + requireNonNullParam(other, "other", "Command.conflictsWith"); + + return !Collections.disjoint(requirements(), other.requirements()); + } + + /** + * Creates a new command that runs this one for a maximum duration, after which it is forcibly + * canceled. This is particularly useful for autonomous routines where you want to prevent your + * entire autonomous period spent stuck on a single action because a mechanism doesn't quite reach + * its setpoint (for example, spinning up a flywheel or driving to a particular location on the + * field). The resulting command will have the same name as this one, with the timeout period + * appended. + * + * @param timeout the maximum duration that the command is permitted to run. Negative or zero + * values will result in the command running only once before being canceled. + * @return the timed out command. + */ + default Command withTimeout(Time timeout) { + requireNonNullParam(timeout, "timeout", "Command.withTimeout"); + + return race(this, waitFor(timeout).named("Timeout: " + timeout.toLongString())) + .named(name() + " [" + timeout.toLongString() + " timeout]"); + } + + /** + * Creates a command that does not require any hardware; that is, it does not affect the state of + * any physical objects. This is useful for commands that do some cleanup or state management, + * such as resetting odometry or sensors, that you don't want to interrupt a command that's + * controlling the mechanisms it affects. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link StagedCommandBuilder} for details. + * + * @return a builder that can be used to configure the resulting command + */ + static NeedsExecutionBuilderStage noRequirements() { + return new StagedCommandBuilder().noRequirements(); + } + + /** + * Creates a command that requires one or more mechanisms. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link StagedCommandBuilder} for details. + * + * @param requirement The first required mechanism + * @param rest Any other required mechanisms + * @return A command builder + */ + static NeedsExecutionBuilderStage requiring(Mechanism requirement, Mechanism... rest) { + // parameters will be null checked by the builder + return new StagedCommandBuilder().requiring(requirement, rest); + } + + /** + * Creates command that requires some number of mechanisms. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link StagedCommandBuilder} for details. + * + * @param requirements The required mechanisms. May be empty, but cannot contain null values. + * @return A command builder + */ + static NeedsExecutionBuilderStage requiring(Collection requirements) { + // parameters will be null checked by the builder + return new StagedCommandBuilder().requiring(requirements); + } + + /** + * Starts creating a command that runs a group of multiple commands in parallel. The command will + * complete when every command in the group has completed naturally. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link ParallelGroupBuilder} for details. + * + * @param commands The commands to run in parallel + * @return A command builder + */ + static ParallelGroupBuilder parallel(Command... commands) { + // parameters will be null checked by the builder + return new ParallelGroupBuilder().requiring(commands); + } + + /** + * Starts creating a command that runs a group of multiple commands in parallel. The command will + * complete when any command in the group has completed naturally; all other commands in the group + * will be canceled. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link ParallelGroupBuilder} for details. + * + * @param commands The commands to run in parallel + * @return A command builder + */ + static ParallelGroupBuilder race(Command... commands) { + // parameters will be null checked by the builder + return new ParallelGroupBuilder().optional(commands); + } + + /** + * Starts creating a command that runs a group of multiple commands in sequence. The command will + * complete when the last command in the group has completed naturally. Commands in the group will + * run in the order they're passed to this method. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link SequentialGroupBuilder} for details. + * + * @param commands The commands to run in sequence. + * @return A command builder + */ + static SequentialGroupBuilder sequence(Command... commands) { + // parameters will be null checked by the builder + return new SequentialGroupBuilder().andThen(commands); + } + + /** + * Starts creating a command that simply waits for some condition to be met. The command will not + * require any mechanisms. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link StagedCommandBuilder} for details. + * + * @param condition The condition to wait for + * @return A command builder + */ + static NeedsNameBuilderStage waitUntil(BooleanSupplier condition) { + requireNonNullParam(condition, "condition", "Command.waitUntil"); + + return noRequirements().executing(coroutine -> coroutine.waitUntil(condition)); + } + + /** + * Creates a command that simply waits for a given duration. The command will not require any + * mechanisms. + * + * @param duration How long to wait + * @return A command builder + */ + static NeedsNameBuilderStage waitFor(Time duration) { + requireNonNullParam(duration, "duration", "Command.waitFor"); + + return noRequirements().executing(coroutine -> coroutine.wait(duration)); + } + + /** + * Creates a command that runs this one and ends when the end condition is met (if this command + * has not already exited by then). + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link ParallelGroupBuilder} for details. + * + * @param endCondition The end condition to wait for. + * @return The waiting command + */ + default ParallelGroupBuilder until(BooleanSupplier endCondition) { + requireNonNullParam(endCondition, "endCondition", "Command.until"); + + return new ParallelGroupBuilder() + .optional(this, Command.waitUntil(endCondition).named("Until Condition")); + } + + /** + * Creates a command sequence, starting from this command and then running the next one. More + * commands can be added with the builder before naming and creating the sequence. + * + *

{@code
+   * Sequence aThenBThenC =
+   *   commandA()
+   *     .andThen(commandB())
+   *     .andThen(commandC())
+   *     .withAutomaticName();
+   * }
+ * + * @param next The command to run after this one in the sequence + * @return A sequence builder + */ + default SequentialGroupBuilder andThen(Command next) { + // parameter will be null checked by the builder + return new SequentialGroupBuilder().andThen(this).andThen(next); + } + + /** + * Creates a parallel command group, running this command alongside one or more other commands. + * The group will exit once every command has finished. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link ParallelGroupBuilder} for details. + * + *

{@code
+   * ParallelGroup abc =
+   *   commandA()
+   *     .alongWith(commandB(), commandC())
+   *     .withAutomaticName();
+   * }
+ * + * @param parallel The commands to run in parallel with this one + * @return A parallel group builder + */ + default ParallelGroupBuilder alongWith(Command... parallel) { + return new ParallelGroupBuilder().requiring(this).requiring(parallel); + } + + /** + * Creates a parallel command group, running this command alongside one or more other commands. + * The group will exit after any command finishes. + * + *

More configuration options are needed after calling this function before the command can be + * created. See {@link ParallelGroupBuilder} for details. + * + * @param parallel The commands to run in parallel with this one + * @return A parallel group builder + */ + default ParallelGroupBuilder raceWith(Command... parallel) { + return new ParallelGroupBuilder().optional(this).optional(parallel); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/CommandState.java b/commandsv3/src/main/java/org/wpilib/commands3/CommandState.java new file mode 100644 index 0000000000..ab10842f13 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/CommandState.java @@ -0,0 +1,100 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static java.util.Objects.requireNonNull; + +/** Represents the state of a command as it is executed by the scheduler. */ +final class CommandState { + // Two billion unique IDs should be enough for anybody. + private static int s_lastId = 0; + + private final Command m_command; + private final Command m_parent; + private final Coroutine m_coroutine; + private final Binding m_binding; + private double m_lastRuntimeMs = -1; + private double m_totalRuntimeMs; + private final int m_id; + + /** + * Creates a new command state object. + * + * @param command The command being tracked. + * @param parent The parent command composition that scheduled the tracked command. Null if the + * command was scheduled by a top level construct like trigger bindings or manually scheduled + * by a user. For deeply nested compositions, this tracks the direct parent command + * that invoked the schedule() call; in this manner, an ancestry tree can be built, where each + * {@code CommandState} object references a parent node in the tree. + * @param coroutine The coroutine to which the command is bound. + * @param binding The binding that caused the command to be scheduled. + */ + CommandState(Command command, Command parent, Coroutine coroutine, Binding binding) { + m_command = requireNonNull(command, "WPILib bug: command state given null command"); + m_parent = parent; // allowed to be null + m_coroutine = requireNonNull(coroutine, "WPILib bug: command state given null coroutine"); + m_binding = requireNonNull(binding, "WPILib bug: command state given null binding"); + + // This is incredibly non-thread safe, but nobody should be using the command framework across + // multiple threads anyway. Worst case scenario, we'll get duplicate IDs for commands scheduled + // by different threads and cause bad telemetry data without affecting program correctness. + m_id = ++s_lastId; + } + + public Command command() { + return m_command; + } + + public Command parent() { + return m_parent; + } + + public Coroutine coroutine() { + return m_coroutine; + } + + public Binding binding() { + return m_binding; + } + + /** + * How long the command took to run the last time it executed. Defaults to -1 if the command has + * not run at least once. + * + * @return The runtime, in milliseconds. + */ + public double lastRuntimeMs() { + return m_lastRuntimeMs; + } + + public void setLastRuntimeMs(double lastRuntimeMs) { + m_lastRuntimeMs = lastRuntimeMs; + m_totalRuntimeMs += lastRuntimeMs; + } + + public double totalRuntimeMs() { + return m_totalRuntimeMs; + } + + public int id() { + return m_id; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CommandState that && m_id == that.m_id; + } + + @Override + public int hashCode() { + return m_id; + } + + @Override + public String toString() { + return "CommandState[command=%s, parent=%s, coroutine=%s, id=%d]" + .formatted(m_command, m_parent, m_coroutine, m_id); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/CommandTraceHelper.java b/commandsv3/src/main/java/org/wpilib/commands3/CommandTraceHelper.java new file mode 100644 index 0000000000..97bcf62476 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/CommandTraceHelper.java @@ -0,0 +1,73 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +final class CommandTraceHelper { + private CommandTraceHelper() { + // Utility class + } + + /** + * Creates a modified stack trace where the trace of the scheduling code replaces the trace of the + * internal scheduler logic. + * + * @param commandExceptionTrace The trace of the exception raised during command execution. + * @param commandScheduleTrace The trace of when the command was scheduled. + * @return A new array of stack trace elements. + */ + public static StackTraceElement[] modifyTrace( + StackTraceElement[] commandExceptionTrace, StackTraceElement[] commandScheduleTrace) { + List frames = new ArrayList<>(); + + List filteredClasses = + List.of( + Coroutine.class.getName(), + Continuation.class.getName(), + Scheduler.class.getName(), + "org.wpilib.commands3.StagedCommandBuilder$BuilderBackedCommand", + "jdk.internal.vm.Continuation"); + + boolean sawRun = false; + for (var exceptionFrame : commandExceptionTrace) { + if (!filteredClasses.contains(exceptionFrame.getClassName())) { + frames.add(exceptionFrame); + } + + // Inject the schedule trace immediately after the line of user code that called Scheduler.run + if (sawRun) { + // Inject a marker frame just so there's a distinction between the command's code and the + // code that scheduled it, since they occur asynchronously + frames.add(new StackTraceElement("=== Command Binding Trace ===", "", "", -1)); + + // Drop internal trigger frames, since they're not helpful for users. + // The first frame here should be the line of user code that bound the command to a trigger. + Stream.of(commandScheduleTrace) + .dropWhile( + frame -> { + boolean inTriggerInternals = frame.getClassName().equals(Trigger.class.getName()); + boolean isScheduleCall = + frame.getClassName().equals(Scheduler.class.getName()) + && "schedule".equals(frame.getMethodName()); + + return inTriggerInternals || isScheduleCall; + }) + .filter(frame -> !filteredClasses.contains(frame.getClassName())) + .forEach(frames::add); + break; + } + + if (exceptionFrame.getClassName().equals(Scheduler.class.getName()) + && "run".equals(exceptionFrame.getMethodName())) { + sawRun = true; + } + } + + return frames.toArray(StackTraceElement[]::new); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/ConflictDetector.java b/commandsv3/src/main/java/org/wpilib/commands3/ConflictDetector.java new file mode 100644 index 0000000000..5be5953fc7 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/ConflictDetector.java @@ -0,0 +1,122 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** Utility class for helping with detecting conflicts between commands. */ +public final class ConflictDetector { + private ConflictDetector() { + // This is a utility class! + } + + /** + * A conflict between two commands. + * + * @param a The first conflicting command. + * @param b The second conflicting command. + * @param sharedRequirements The set of mechanisms required by both commands. This set is + * read-only + */ + public record Conflict(Command a, Command b, Set sharedRequirements) { + /** + * Gets a descriptive message for the conflict. The description includes the names of the + * conflicting commands and the names of all mechanisms required by both commands. + * + * @return A description of the conflict. + */ + public String description() { + var shared = + sharedRequirements.stream() + .map(Mechanism::getName) + .sorted() + .collect(Collectors.joining(", ")); + return "%s and %s both require %s".formatted(a.name(), b.name(), shared); + } + } + + /** + * Validates that a set of commands have no internal requirement conflicts. An error is thrown if + * a conflict is detected. + * + * @param commands The commands to validate + * @throws IllegalArgumentException If at least one pair of commands is found in the input where + * both commands have at least one required mechanism in common + */ + public static void throwIfConflicts(Collection commands) { + requireNonNullParam(commands, "commands", "ConflictDetector.throwIfConflicts"); + + var conflicts = findAllConflicts(commands); + if (conflicts.isEmpty()) { + // No conflicts, all good + return; + } + + StringBuilder message = + new StringBuilder("Commands running in parallel cannot share requirements: "); + for (int i = 0; i < conflicts.size(); i++) { + Conflict conflict = conflicts.get(i); + message.append(conflict.description()); + if (i < conflicts.size() - 1) { + message.append("; "); + } + } + + throw new IllegalArgumentException(message.toString()); + } + + /** + * Finds all conflicting pairs of commands in the input collection. + * + * @param commands The commands to check. + * @return All detected conflicts. The returned list is empty if no conflicts were found. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public static List findAllConflicts(Collection commands) { + requireNonNullParam(commands, "commands", "ConflictDetector.findAllConflicts"); + + List conflicts = new ArrayList<>(); + + int i = 0; + for (Command command : commands) { + i++; + int j = 0; + for (Command other : commands) { + j++; + if (j <= i) { + // Skip all elements in 0..i so the inner loop only checks elements from i + 1 onward. + // Ordering of the elements in the pair doesn't matter, and this prevents finding every + // pair twice eg (A, B) and (B, A). + continue; + } + + if (command == other) { + // Commands cannot conflict with themselves, so just in case the input collection happens + // to have duplicate elements we just skip any pairs of a command with itself. + continue; + } + + if (command.conflictsWith(other)) { + conflicts.add(findConflict(command, other)); + } + } + } + + return conflicts; + } + + private static Conflict findConflict(Command a, Command b) { + Set sharedRequirements = new HashSet<>(a.requirements()); + sharedRequirements.retainAll(b.requirements()); + return new Conflict(a, b, Set.copyOf(sharedRequirements)); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/Continuation.java b/commandsv3/src/main/java/org/wpilib/commands3/Continuation.java new file mode 100644 index 0000000000..6a0f9aa907 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/Continuation.java @@ -0,0 +1,230 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.WrongMethodTypeException; + +/** + * A wrapper around the JDK internal Continuation class. Continuations are one-shot (i.e., not + * reusable after completion) and allow stack frames to be paused and resumed at a later time. They + * are the underpinning for virtual threads, which have their own scheduler and JVM support. Bare + * continuations are designed for internal use by the JVM; we have forcibly opened access to them + * for use by the commands framework due to limitations of virtual threads; notably, their complete + * lack of determinism and timing, which are critically important for real-time systems like robots. + * + *

ONLY USE CONTINUATIONS IN A SINGLE THREADED CONTEXT. The JVM and JIT + * compilers make fundamental assumptions about how continuations are used, and rely on the code + * that uses it (which was intended to be virtual threads) to use it safely. Failure to use + * this API safely can result in JIT compilers to issue invalid code causing buggy behavior and JVM + * crashes at any time, up to and including on a field during an official match. + * + *

Teams don't need to use continuations directly with the commands framework; all mounting and + * unmounting is handled by the command scheduler and a coroutine wrapper. + */ +final class Continuation { + // The underlying jdk.internal.vm.Continuation object + // https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/jdk/internal/vm/Continuation.java + private final Object m_continuation; + + static final Class jdk_internal_vm_Continuation; + private static final MethodHandle CONSTRUCTOR; + + @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") + private static final MethodHandle YIELD; + + @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") + private static final MethodHandle RUN; + + private static final MethodHandle IS_DONE; + + private static final MethodHandle java_lang_thread_setContinuation; + + private static final ThreadLocal mountedContinuation = new ThreadLocal<>(); + + static { + try { + jdk_internal_vm_Continuation = Class.forName("jdk.internal.vm.Continuation"); + + var lookup = + MethodHandles.privateLookupIn(jdk_internal_vm_Continuation, MethodHandles.lookup()); + + CONSTRUCTOR = + lookup.findConstructor( + jdk_internal_vm_Continuation, + MethodType.methodType( + void.class, ContinuationScope.jdk_internal_vm_ContinuationScope, Runnable.class)); + + YIELD = + lookup.findStatic( + jdk_internal_vm_Continuation, + "yield", + MethodType.methodType( + boolean.class, ContinuationScope.jdk_internal_vm_ContinuationScope)); + + RUN = + lookup.findVirtual( + jdk_internal_vm_Continuation, "run", MethodType.methodType(void.class)); + + IS_DONE = + lookup.findVirtual( + jdk_internal_vm_Continuation, "isDone", MethodType.methodType(boolean.class)); + } catch (Throwable t) { + throw new ExceptionInInitializerError(t); + } + } + + static { + try { + var lookup = MethodHandles.privateLookupIn(Thread.class, MethodHandles.lookup()); + + java_lang_thread_setContinuation = + lookup.findVirtual( + Thread.class, + "setContinuation", + MethodType.methodType(void.class, Continuation.jdk_internal_vm_Continuation)); + } catch (Throwable t) { + throw new ExceptionInInitializerError(t); + } + } + + /** + * Used to wrap any checked exceptions bubbled from when calling the internal continuation methods + * via reflection. Per the Continuation API as of Java 21, none of the methods we're calling will + * throw unchecked exceptions (IllegalState or other runtime exceptions, yes, and we bubble those + * up directly); however, the MethodHandle API's `invoke` method has `throws Throwable` in its + * signature and we have to handle it. + */ + @SuppressWarnings("PMD.DoNotExtendJavaLangError") + static final class InternalContinuationError extends Error { + InternalContinuationError(String message, Throwable cause) { + super(message, cause); + } + } + + private final ContinuationScope m_scope; + + /** + * Constructs a continuation. + * + * @param scope the continuation's scope, used in yield + * @param target the continuation's body + */ + @SuppressWarnings({"PMD.AvoidRethrowingException", "PMD.AvoidCatchingGenericException"}) + Continuation(ContinuationScope scope, Runnable target) { + try { + m_continuation = CONSTRUCTOR.invoke(scope.m_continuationScope, target); + m_scope = scope; + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + /** + * Suspends the current continuations up to this continuation's scope. + * + * @return {@code true} for success; {@code false} for failure + * @throws IllegalStateException if not currently in this continuation's scope + */ + @SuppressWarnings({"PMD.AvoidRethrowingException", "PMD.AvoidCatchingGenericException"}) + public boolean yield() { + try { + return (boolean) YIELD.invoke(m_scope.m_continuationScope); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new InternalContinuationError( + "Continuation.yield() encountered an unexpected error", t); + } + } + + /** + * Mounts and runs the continuation body until the body calls {@link #yield()}. If the + * continuation is suspended, it will continue from the most recent yield point. + */ + @SuppressWarnings({"PMD.AvoidRethrowingException", "PMD.AvoidCatchingGenericException"}) + public void run() { + try { + RUN.invoke(m_continuation); + } catch (WrongMethodTypeException | ClassCastException e) { + throw new IllegalStateException("Unable to run the underlying continuation!", e); + } catch (RuntimeException | Error e) { + // The bound task threw an exception; re-throw it + throw e; + } catch (Throwable t) { + throw new InternalContinuationError("Continuation.run() encountered an unexpected error", t); + } + } + + /** + * Tests whether this continuation is completed. + * + * @return whether this continuation is completed + */ + @SuppressWarnings({"PMD.AvoidRethrowingException", "PMD.AvoidCatchingGenericException"}) + public boolean isDone() { + try { + return (boolean) IS_DONE.invoke(m_continuation); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + throw new InternalContinuationError( + "Continuation.isDone() encountered an unexpected error", t); + } + } + + /** + * Mounds a continuation to the current thread. Accepts null for clearing the currently mounted + * continuation. + * + * @param continuation the continuation to mount + */ + @SuppressWarnings({"PMD.AvoidRethrowingException", "PMD.AvoidCatchingGenericException"}) + public static void mountContinuation(Continuation continuation) { + try { + if (continuation == null) { + java_lang_thread_setContinuation.invoke(Thread.currentThread(), null); + mountedContinuation.remove(); + } else { + java_lang_thread_setContinuation.invoke( + Thread.currentThread(), continuation.m_continuation); + mountedContinuation.set(continuation); + } + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable t) { + // `t` is anything thrown internally by Thread.setContinuation. + // It only assigns to a field, no way to throw + // However, if the invocation fails for some reason, we'll end up with an + // IllegalStateException when attempting to run an unmounted continuation + throw new InternalContinuationError( + "Continuation.mountContinuation() encountered an unexpected error", t); + } + } + + /** + * Gets the currently mounted continuation. This is thread-local value; calling this method on two + * different threads will give two different results. + * + * @return The continuation mounted on the current thread. + */ + public static Continuation getMountedContinuation() { + return mountedContinuation.get(); + } + + @Override + public String toString() { + return m_continuation.toString(); + } + + @SuppressWarnings("PMD.CompareObjectsWithEquals") + boolean isMounted() { + return this == getMountedContinuation(); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/ContinuationScope.java b/commandsv3/src/main/java/org/wpilib/commands3/ContinuationScope.java new file mode 100644 index 0000000000..27484d88fc --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/ContinuationScope.java @@ -0,0 +1,54 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Objects; + +/** A continuation scope. */ +final class ContinuationScope { + // The underlying jdk.internal.vm.ContinuationScope object + // https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/jdk/internal/vm/ContinuationScope.java + final Object m_continuationScope; + + static final Class jdk_internal_vm_ContinuationScope; + private static final MethodHandle CONSTRUCTOR; + + static { + try { + jdk_internal_vm_ContinuationScope = Class.forName("jdk.internal.vm.ContinuationScope"); + + var lookup = + MethodHandles.privateLookupIn(jdk_internal_vm_ContinuationScope, MethodHandles.lookup()); + + CONSTRUCTOR = + lookup.findConstructor( + jdk_internal_vm_ContinuationScope, MethodType.methodType(void.class, String.class)); + } catch (Throwable t) { + throw new ExceptionInInitializerError(t); + } + } + + /** + * Constructs a new scope. + * + * @param name The scope's name + */ + ContinuationScope(String name) { + Objects.requireNonNull(name); + try { + m_continuationScope = CONSTRUCTOR.invoke(name); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return m_continuationScope.toString(); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/Coroutine.java b/commandsv3/src/main/java/org/wpilib/commands3/Coroutine.java new file mode 100644 index 0000000000..71e159e67a --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/Coroutine.java @@ -0,0 +1,377 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.units.Units.Seconds; +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import edu.wpi.first.units.measure.Time; +import edu.wpi.first.wpilibj.Timer; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +/** + * A coroutine object is injected into command's {@link Command#run(Coroutine)} method to allow + * commands to yield and compositions to run other commands. Commands are considered bound to + * a coroutine while they're scheduled; attempting to use a coroutine outside the command bound to + * it will result in an {@code IllegalStateException} being thrown. + */ +public final class Coroutine { + private final Scheduler m_scheduler; + private final Continuation m_backingContinuation; + + /** + * Creates a new coroutine. Package-private; only the scheduler should be creating these. + * + * @param scheduler The scheduler running the coroutine + * @param scope The continuation scope the coroutine's backing continuation runs in + * @param callback The callback for the continuation to execute when mounted. Often a command + * function's body. + */ + Coroutine(Scheduler scheduler, ContinuationScope scope, Consumer callback) { + m_scheduler = scheduler; + m_backingContinuation = new Continuation(scope, () -> callback.accept(this)); + } + + /** + * Yields control back to the scheduler to allow other commands to execute. This can be thought of + * as "pausing" the currently executing command. + * + * @return true + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public boolean yield() { + requireMounted(); + + try { + return m_backingContinuation.yield(); + } catch (IllegalStateException e) { + if ("Pinned: MONITOR".equals(e.getMessage())) { + // Raised when a continuation yields inside a synchronized block or method: + // https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/jdk/internal/vm/Continuation.java#L396-L402 + // Note: Not a thing in Java 24+ + // Rethrow with an error message that's more helpful for our users + throw new IllegalStateException( + "Coroutine.yield() cannot be called inside a synchronized block or method. " + + "Consider using a Lock instead of synchronized, " + + "or rewrite your code to avoid locks and mutexes altogether.", + e); + } else { + // rethrow + throw e; + } + } + } + + /** + * Parks the current command. No code in a command declared after calling {@code park()} will be + * executed. A parked command will never complete naturally and must be interrupted or canceled. + * + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + @SuppressWarnings("InfiniteLoopStatement") + public void park() { + requireMounted(); + + while (true) { + // 'this' is required because 'yield' is a semi-keyword and needs to be qualified + this.yield(); + } + } + + /** + * Schedules a child command and then immediately returns. The child command will run until its + * natural completion, the parent command exits, or the parent command cancels it. + * + *

This is a nonblocking operation. To fork and then wait for the child command to complete, + * use {@link #await(Command)}. + * + *

The parent command will continue executing while the child command runs, and can resync with + * the child command using {@link #await(Command)}. + * + *

{@code
+   * Command example() {
+   *   return Command.noRequirements().executing(coroutine -> {
+   *     Command child = ...;
+   *     coroutine.fork(child);
+   *     // ... do more things
+   *     // then sync back up with the child command
+   *     coroutine.await(child);
+   *   }).named("Example");
+   * }
+   * }
+ * + *

Note: forking a command that conflicts with a higher-priority command will fail. The forked + * command will not be scheduled, and the existing command will continue to run. + * + * @param commands The commands to fork. + * @throws IllegalStateException if called anywhere other than the coroutine's running command + * @see #await(Command) + */ + public void fork(Command... commands) { + requireMounted(); + + requireNonNullParam(commands, "commands", "Coroutine.fork"); + for (int i = 0; i < commands.length; i++) { + requireNonNullParam(commands[i], "commands[" + i + "]", "Coroutine.fork"); + } + + // Check for user error; there's no reason to fork conflicting commands simultaneously + ConflictDetector.throwIfConflicts(List.of(commands)); + + // Shorthand; this is handy for user-defined compositions + for (var command : commands) { + m_scheduler.schedule(command); + } + } + + /** + * Forks off some commands. Each command will run until its natural completion, the parent command + * exits, or the parent command cancels it. The parent command will continue executing while the + * forked commands run, and can resync with the forked commands using {@link + * #awaitAll(Collection)}. + * + *

{@code
+   * Command example() {
+   *   return Command.noRequirements().executing(coroutine -> {
+   *     Collection innerCommands = ...;
+   *     coroutine.fork(innerCommands);
+   *     // ... do more things
+   *     // then sync back up with the inner commands
+   *     coroutine.awaitAll(innerCommands);
+   *   }).named("Example");
+   * }
+   * }
+ * + *

Note: forking a command that conflicts with a higher-priority command will fail. The forked + * command will not be scheduled, and the existing command will continue to run. + * + * @param commands The commands to fork. + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public void fork(Collection commands) { + fork(commands.toArray(Command[]::new)); + } + + /** + * Awaits completion of a command. If the command is not currently scheduled or running, it will + * be scheduled automatically. This is a blocking operation and will not return until the command + * completes or has been interrupted by another command scheduled by the same parent. + * + * @param command the command to await + * @throws IllegalStateException if called anywhere other than the coroutine's running command + * @see #fork(Command...) + */ + public void await(Command command) { + requireMounted(); + + requireNonNullParam(command, "command", "Coroutine.await"); + + m_scheduler.schedule(command); + + while (m_scheduler.isScheduledOrRunning(command)) { + // If the command is a one-shot, then the schedule call will completely execute the command. + // There would be nothing to await + this.yield(); + } + } + + /** + * Awaits completion of all given commands. If any command is not current scheduled or running, it + * will be scheduled. + * + * @param commands the commands to await + * @throws IllegalArgumentException if any of the commands conflict with each other + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public void awaitAll(Collection commands) { + requireMounted(); + + requireNonNullParam(commands, "commands", "Coroutine.awaitAll"); + int i = 0; + for (Command command : commands) { + requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAll"); + i++; + } + + ConflictDetector.throwIfConflicts(commands); + + for (var command : commands) { + m_scheduler.schedule(command); + } + + while (commands.stream().anyMatch(m_scheduler::isScheduledOrRunning)) { + this.yield(); + } + } + + /** + * Awaits completion of all given commands. If any command is not current scheduled or running, it + * will be scheduled. + * + * @param commands the commands to await + * @throws IllegalArgumentException if any of the commands conflict with each other + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public void awaitAll(Command... commands) { + awaitAll(Arrays.asList(commands)); + } + + /** + * Awaits completion of any given commands. Any command that's not already scheduled or running + * will be scheduled. After any of the given commands completes, the rest will be canceled. + * + * @param commands the commands to await + * @throws IllegalArgumentException if any of the commands conflict with each other + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public void awaitAny(Collection commands) { + requireMounted(); + + requireNonNullParam(commands, "commands", "Coroutine.awaitAny"); + int i = 0; + for (Command command : commands) { + requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAny"); + i++; + } + + ConflictDetector.throwIfConflicts(commands); + + // Schedule anything that's not already queued or running + for (var command : commands) { + m_scheduler.schedule(command); + } + + while (commands.stream().allMatch(m_scheduler::isScheduledOrRunning)) { + this.yield(); + } + + // At least one command exited; cancel the rest. + commands.forEach(m_scheduler::cancel); + } + + /** + * Awaits completion of any given commands. Any command that's not already scheduled or running + * will be scheduled. After any of the given commands completes, the rest will be canceled. + * + * @param commands the commands to await + * @throws IllegalArgumentException if any of the commands conflict with each other + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public void awaitAny(Command... commands) { + awaitAny(Arrays.asList(commands)); + } + + /** + * Waits for some duration of time to elapse. Returns immediately if the given duration is zero or + * negative. Call this within a command or command composition to introduce a simple delay. + * + *

For example, a basic autonomous routine that drives straight for 5 seconds: + * + *

{@code
+   * Command timedDrive() {
+   *   return drivebase.run(coroutine -> {
+   *     drivebase.tankDrive(1, 1);
+   *     coroutine.wait(Seconds.of(5));
+   *     drivebase.stop();
+   *   }).named("Timed Drive");
+   * }
+   * }
+ * + *

Note that the resolution of the wait period is equal to the period at which {@link + * Scheduler#run()} is called by the robot program. If using a 20 millisecond update period, the + * wait will be rounded up to the nearest 20 millisecond interval; in this scenario, calling + * {@code wait(Milliseconds.of(1))} and {@code wait(Milliseconds.of(19))} would have identical + * effects. + * + *

Very small loop times near the loop period are sensitive to the order in which commands are + * executed. If a command waits for 10 ms at the end of a scheduler cycle, and then all the + * commands that ran before it complete or exit, and then the next cycle starts immediately, the + * wait will be evaluated at the start of that next cycle, which occurred less than 10 ms + * later. Therefore, the wait will see not enough time has passed and only exit after an + * additional cycle elapses, adding an unexpected extra 20 ms to the wait time. This becomes less + * of a problem with smaller loop periods, as the extra 1-loop delay becomes smaller. + * + * @param duration the duration of time to wait + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public void wait(Time duration) { + requireMounted(); + + requireNonNullParam(duration, "duration", "Coroutine.wait"); + + var timer = new Timer(); + timer.start(); + while (!timer.hasElapsed(duration.in(Seconds))) { + this.yield(); + } + } + + /** + * Yields until a condition is met. + * + * @param condition The condition to wait for + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public void waitUntil(BooleanSupplier condition) { + requireMounted(); + + requireNonNullParam(condition, "condition", "Coroutine.waitUntil"); + + while (!condition.getAsBoolean()) { + this.yield(); + } + } + + /** + * Advanced users only: this permits access to the backing command scheduler to run custom logic + * not provided by the standard coroutine methods. Any commands manually scheduled through this + * will be canceled when the parent command exits - it's not possible to "fork" a command that + * lives longer than the command that scheduled it. + * + * @return the command scheduler backing this coroutine + * @throws IllegalStateException if called anywhere other than the coroutine's running command + */ + public Scheduler scheduler() { + requireMounted(); + + return m_scheduler; + } + + private boolean isMounted() { + return m_backingContinuation.isMounted(); + } + + private void requireMounted() { + // Note: attempting to yield() outside a command will already throw an error due to the + // continuation being unmounted, but other actions like forking and awaiting should also + // throw errors. For consistent messaging, we use this helper in all places, not just the + // ones that interact with the backing continuation. + + if (isMounted()) { + return; + } + + throw new IllegalStateException("Coroutines can only be used by the command bound to them"); + } + + // Package-private for interaction with the scheduler. + // These functions are not intended for team use. + + void runToYieldPoint() { + m_backingContinuation.run(); + } + + void mount() { + Continuation.mountContinuation(m_backingContinuation); + } + + boolean isDone() { + return m_backingContinuation.isDone(); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/Mechanism.java b/commandsv3/src/main/java/org/wpilib/commands3/Mechanism.java new file mode 100644 index 0000000000..4d8e41ac4c --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/Mechanism.java @@ -0,0 +1,165 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import edu.wpi.first.units.measure.Time; +import java.util.List; +import java.util.function.Consumer; +import org.wpilib.annotation.NoDiscard; + +/** + * Generic base class to represent mechanisms on a robot. Commands can require sole ownership of a + * mechanism; when a command that requires a mechanism is running, no other commands may use it at + * the same time. + * + *

Even though this class is named "Mechanism", it may be used to represent other physical + * hardware on a robot that should be controlled with commands - for example, an LED strip or a + * vision processor that can switch between different pipelines could be represented as mechanisms. + */ +public class Mechanism { + private final String m_name; + + private final Scheduler m_registeredScheduler; + + /** + * Creates a new mechanism registered with the default scheduler instance and named using the name + * of the class. Intended to be used by subclasses to get sane defaults without needing to + * manually declare a constructor. + */ + @SuppressWarnings("this-escape") + protected Mechanism() { + m_name = getClass().getSimpleName(); + m_registeredScheduler = Scheduler.getDefault(); + setDefaultCommand(idle()); + } + + /** + * Creates a new mechanism, registered with the default scheduler instance. + * + * @param name The name of the mechanism. Cannot be null. + */ + public Mechanism(String name) { + this(name, Scheduler.getDefault()); + } + + /** + * Creates a new mechanism, registered with the given scheduler instance. + * + * @param name The name of the mechanism. Cannot be null. + * @param scheduler The registered scheduler. Cannot be null. + */ + @SuppressWarnings("this-escape") + public Mechanism(String name, Scheduler scheduler) { + m_name = name; + m_registeredScheduler = scheduler; + setDefaultCommand(idle()); + } + + /** + * Gets the name of this mechanism. + * + * @return The name of the mechanism. + */ + @NoDiscard + public String getName() { + return m_name; + } + + /** + * Sets the default command to run on the mechanism when no other command is scheduled. The + * default command's priority is effectively the minimum allowable priority for any command + * requiring a mechanism. For this reason, it's recommended that a default command have a priority + * less than {@link Command#DEFAULT_PRIORITY} so it doesn't prevent low-priority commands from + * running. + * + *

The default command is initially an idle command that only owns the mechanism without doing + * anything. This command has the lowest possible priority to allow any other command to run. + * + * @param defaultCommand the new default command + */ + public void setDefaultCommand(Command defaultCommand) { + m_registeredScheduler.setDefaultCommand(this, defaultCommand); + } + + /** + * Gets the default command that was set by the latest call to {@link + * #setDefaultCommand(Command)}. + * + * @return The currently configured default command + */ + public Command getDefaultCommand() { + return m_registeredScheduler.getDefaultCommandFor(this); + } + + /** + * Starts building a command that requires this mechanism. + * + * @param commandBody The main function body of the command. + * @return The command builder, for further configuration. + */ + public NeedsNameBuilderStage run(Consumer commandBody) { + return new StagedCommandBuilder().requiring(this).executing(commandBody); + } + + /** + * Starts building a command that requires this mechanism. The given function will be called + * repeatedly in an infinite loop. Useful for building commands that don't need state or multiple + * stages of logic. + * + * @param loopBody The body of the infinite loop. + * @return The command builder, for further configuration. + */ + public NeedsNameBuilderStage runRepeatedly(Runnable loopBody) { + return run( + coroutine -> { + while (true) { + loopBody.run(); + coroutine.yield(); + } + }); + } + + /** + * Returns a command that idles this mechanism until another command claims it. The idle command + * has {@link Command#LOWEST_PRIORITY the lowest priority} and can be interrupted by any other + * command. + * + *

The {@link #getDefaultCommand() default command} for every mechanism is an idle command + * unless a different default command has been configured. + * + * @return A new idle command. + */ + public Command idle() { + return run(Coroutine::park).withPriority(Command.LOWEST_PRIORITY).named(getName() + "[IDLE]"); + } + + /** + * Returns a command that idles this mechanism for the given duration of time. + * + * @param duration How long the mechanism should idle for. + * @return A new idle command. + */ + public Command idleFor(Time duration) { + return idle().withTimeout(duration); + } + + /** + * Gets all running commands that require this mechanism. Commands are returned in the order in + * which they were scheduled. The returned list is read-only. Every command in the list will have + * been scheduled by the previous entry in the list or by intermediate commands that do not + * require the mechanism. + * + * @return The currently running commands that require the mechanism. + */ + @NoDiscard + public List getRunningCommands() { + return m_registeredScheduler.getRunningCommandsFor(this); + } + + @Override + public String toString() { + return m_name; + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/NeedsExecutionBuilderStage.java b/commandsv3/src/main/java/org/wpilib/commands3/NeedsExecutionBuilderStage.java new file mode 100644 index 0000000000..23b5b2f641 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/NeedsExecutionBuilderStage.java @@ -0,0 +1,56 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import java.util.Collection; +import java.util.function.Consumer; +import org.wpilib.annotation.NoDiscard; + +/** + * A stage in a command builder where requirements have already been specified and execution details + * are required. The next stage is a {@link NeedsNameBuilderStage} from {@link + * #executing(Consumer)}. Additional requirements may still be added before moving on to the next + * stage. + */ +@NoDiscard +public interface NeedsExecutionBuilderStage { + /** + * Adds a required mechanism for the command. + * + * @param requirement A required mechanism. Cannot be null. + * @return This builder object, for chaining + * @throws NullPointerException If {@code requirement} is null + */ + NeedsExecutionBuilderStage requiring(Mechanism requirement); + + /** + * Adds one or more required mechanisms for the command. + * + * @param requirement A required mechanism. Cannot be null. + * @param extra Any extra required mechanisms. May be empty, but cannot contain null values. + * @return This builder object, for chaining + * @throws NullPointerException If {@code requirement} is null or {@code extra} contains a null + * value + */ + NeedsExecutionBuilderStage requiring(Mechanism requirement, Mechanism... extra); + + /** + * Adds required mechanisms for the command. + * + * @param requirements Any required mechanisms. May be empty, but cannot contain null values. + * @return This builder object, for chaining + * @throws NullPointerException If {@code requirements} is null or contains a null value. + */ + NeedsExecutionBuilderStage requiring(Collection requirements); + + /** + * Sets the function body of the executing command. + * + * @param impl The command's body. Cannot be null. + * @return A builder for the next stage of command construction. + * @throws NullPointerException If {@code impl} is null. + */ + NeedsNameBuilderStage executing(Consumer impl); +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/NeedsNameBuilderStage.java b/commandsv3/src/main/java/org/wpilib/commands3/NeedsNameBuilderStage.java new file mode 100644 index 0000000000..d89691a6ba --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/NeedsNameBuilderStage.java @@ -0,0 +1,56 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import org.wpilib.annotation.NoDiscard; + +/** + * A stage in a command builder where requirements and main command execution logic have been set. + * No more changes to requirements or command implementation may happen after this point. This is + * the final step in command creation + */ +@NoDiscard +public interface NeedsNameBuilderStage { + /** + * Optionally sets a callback to execute when the command is canceled. The callback will not run + * if the command was canceled after being scheduled but before starting to run, and will not run + * if the command completes naturally or from encountering an unhandled exception. + * + * @param onCancel The function to execute when the command is canceled while running. May be + * null. + * @return This builder object, for chaining + */ + NeedsNameBuilderStage whenCanceled(Runnable onCancel); + + /** + * Sets the priority level of the command. + * + * @param priority The desired priority level. + * @return This builder object, for chaining. + */ + NeedsNameBuilderStage withPriority(int priority); + + /** + * Optionally sets an end condition for the command. If the end condition returns {@code true} + * before the command body set in {@link NeedsExecutionBuilderStage#executing(Consumer)} would + * exit, the command will be canceled by the scheduler. + * + * @param endCondition An optional end condition for the command. May be null. + * @return This builder object, for chaining. + */ + NeedsNameBuilderStage until(BooleanSupplier endCondition); + + /** + * Creates the command based on the options set during previous builder stages. The builders will + * no longer be usable after calling this method. + * + * @param name The name of the command + * @return The built command. + * @throws NullPointerException If {@code name} is null. + */ + Command named(String name); +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/ParallelGroup.java b/commandsv3/src/main/java/org/wpilib/commands3/ParallelGroup.java new file mode 100644 index 0000000000..75025c2b28 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/ParallelGroup.java @@ -0,0 +1,107 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * A type of command that runs multiple other commands in parallel. The group will end after all + * required commands have completed; if no command is explicitly required for completion, then the + * group will end after any command in the group finishes. Any commands still running when the group + * has reached its completion condition will be canceled. + */ +public final class ParallelGroup implements Command { + private final Collection m_requiredCommands = new HashSet<>(); + private final Collection m_optionalCommands = new LinkedHashSet<>(); + private final Set m_requirements = new HashSet<>(); + private final String m_name; + private final int m_priority; + + /** + * Creates a new parallel group. + * + * @param name The name of the group. + * @param requiredCommands The commands that are required to complete for the group to finish. If + * this is empty, then the group will finish when any optional command completes. + * @param optionalCommands The commands that do not need to complete for the group to finish. If + * this is empty, then the group will finish when all required commands complete. + */ + ParallelGroup( + String name, Collection requiredCommands, Collection optionalCommands) { + requireNonNullParam(name, "name", "ParallelGroup"); + requireNonNullParam(requiredCommands, "requiredCommands", "ParallelGroup"); + requireNonNullParam(optionalCommands, "optionalCommands", "ParallelGroup"); + + int i = 0; + for (Command requiredCommand : requiredCommands) { + requireNonNullParam(requiredCommand, "requiredCommands[" + i + "]", "ParallelGroup"); + i++; + } + + i = 0; + for (Command c : optionalCommands) { + requireNonNullParam(c, "optionalCommands[" + i + "]", "ParallelGroup"); + i++; + } + + var allCommands = new LinkedHashSet(); + allCommands.addAll(requiredCommands); + allCommands.addAll(optionalCommands); + + ConflictDetector.throwIfConflicts(allCommands); + + m_name = name; + m_optionalCommands.addAll(optionalCommands); + m_requiredCommands.addAll(requiredCommands); + + for (var command : allCommands) { + m_requirements.addAll(command.requirements()); + } + + m_priority = + allCommands.stream().mapToInt(Command::priority).max().orElse(Command.DEFAULT_PRIORITY); + } + + @Override + public void run(Coroutine coroutine) { + coroutine.fork(m_optionalCommands); + + if (m_requiredCommands.isEmpty()) { + // No required commands - just wait for the first optional command to finish + coroutine.awaitAny(m_optionalCommands); + } else { + // Wait for every required command to finish + coroutine.awaitAll(m_requiredCommands); + } + + // The scheduler will cancel any optional child commands that are still running when + // the `run` method returns + } + + @Override + public String name() { + return m_name; + } + + @Override + public Set requirements() { + return m_requirements; + } + + @Override + public int priority() { + return m_priority; + } + + @Override + public String toString() { + return "ParallelGroup[name=" + m_name + "]"; + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/ParallelGroupBuilder.java b/commandsv3/src/main/java/org/wpilib/commands3/ParallelGroupBuilder.java new file mode 100644 index 0000000000..e5660ebd23 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/ParallelGroupBuilder.java @@ -0,0 +1,147 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; +import org.wpilib.annotation.NoDiscard; + +/** + * A builder class to configure and then create a {@link ParallelGroup}. Like {@link + * StagedCommandBuilder}, the final command is created by calling the terminal {@link + * #named(String)} method, or with an automatically generated name using {@link + * #withAutomaticName()}. + */ +@NoDiscard +public class ParallelGroupBuilder { + private final Set m_commands = new LinkedHashSet<>(); + private final Set m_requiredCommands = new LinkedHashSet<>(); + private BooleanSupplier m_endCondition; + + /** + * Creates a new parallel group builder. The builder will have no commands and have no preapplied + * configuration options. + */ + public ParallelGroupBuilder() {} + + /** + * Adds one or more optional commands to the group. They will not be required to complete for the + * parallel group to exit, and will be canceled once all required commands have finished. + * + * @param commands The optional commands to add to the group + * @return The builder object, for chaining + */ + public ParallelGroupBuilder optional(Command... commands) { + requireNonNullParam(commands, "commands", "ParallelGroupBuilder.optional"); + for (int i = 0; i < commands.length; i++) { + requireNonNullParam(commands[i], "commands[" + i + "]", "ParallelGroupBuilder.optional"); + } + + m_commands.addAll(Arrays.asList(commands)); + return this; + } + + /** + * Adds one or more required commands to the group. All required commands will need to complete + * for the group to exit. + * + * @param commands The required commands to add to the group + * @return The builder object, for chaining + */ + public ParallelGroupBuilder requiring(Command... commands) { + requireNonNullParam(commands, "commands", "ParallelGroupBuilder.requiring"); + for (int i = 0; i < commands.length; i++) { + requireNonNullParam(commands[i], "commands[" + i + "]", "ParallelGroupBuilder.requiring"); + } + + m_requiredCommands.addAll(Arrays.asList(commands)); + m_commands.addAll(m_requiredCommands); + return this; + } + + /** + * Adds a command to the group. The command must complete for the group to exit. + * + * @param command The command to add to the group + * @return The builder object, for chaining + */ + // Note: this primarily exists so users can cleanly chain .alongWith calls to build a + // parallel group, eg `fooCommand().alongWith(barCommand()).alongWith(bazCommand())` + public ParallelGroupBuilder alongWith(Command command) { + return requiring(command); + } + + /** + * Adds an end condition to the command group. If this condition is met before all required + * commands have completed, the group will exit early. If multiple end conditions are added (e.g. + * {@code .until(() -> conditionA()).until(() -> conditionB())}), then the last end condition + * added will be used and any previously configured condition will be overridden. + * + * @param condition The end condition for the group. May be null. + * @return The builder object, for chaining + */ + public ParallelGroupBuilder until(BooleanSupplier condition) { + m_endCondition = condition; + return this; + } + + /** + * Creates the group, using the provided named. The group will require everything that the + * commands in the group require, and will have a priority level equal to the highest priority + * among those commands. + * + * @param name The name of the parallel group + * @return The built group + */ + public ParallelGroup named(String name) { + requireNonNullParam(name, "name", "ParallelGroupBuilder.named"); + + var group = new ParallelGroup(name, m_commands, m_requiredCommands); + if (m_endCondition == null) { + // No custom end condition, return the group as is + return group; + } + + // We have a custom end condition, so we need to wrap the group in a race + return new ParallelGroupBuilder() + .optional(group, Command.waitUntil(m_endCondition).named("Until Condition")) + .named(name); + } + + /** + * Creates the group, giving it a name based on the commands within the group. + * + * @return The built group + */ + public ParallelGroup withAutomaticName() { + // eg "(CommandA & CommandB & CommandC)" + String required = + m_requiredCommands.stream().map(Command::name).collect(Collectors.joining(" & ", "(", ")")); + + var optionalCommands = new LinkedHashSet<>(m_commands); + optionalCommands.removeAll(m_requiredCommands); + // eg "(CommandA | CommandB | CommandC)" + String optional = + optionalCommands.stream().map(Command::name).collect(Collectors.joining(" | ", "(", ")")); + + if (m_requiredCommands.isEmpty()) { + // No required commands, pure race + return named(optional); + } else if (optionalCommands.isEmpty()) { + // Everything required + return named(required); + } else { + // Some form of deadline + // eg "[(CommandA & CommandB) * (CommandX | CommandY | ...)]" + String name = "[%s * %s]".formatted(required, optional); + return named(name); + } + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/Scheduler.java b/commandsv3/src/main/java/org/wpilib/commands3/Scheduler.java new file mode 100644 index 0000000000..752b779253 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/Scheduler.java @@ -0,0 +1,970 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.units.Units.Microseconds; +import static edu.wpi.first.units.Units.Milliseconds; + +import edu.wpi.first.util.ErrorMessages; +import edu.wpi.first.util.protobuf.ProtobufSerializable; +import edu.wpi.first.wpilibj.RobotController; +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.event.EventLoop; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.SequencedMap; +import java.util.SequencedSet; +import java.util.Set; +import java.util.Stack; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.wpilib.annotation.NoDiscard; +import org.wpilib.commands3.button.CommandGenericHID; +import org.wpilib.commands3.proto.SchedulerProto; + +/** + * Manages the lifecycles of {@link Coroutine}-based {@link Command Commands}. Commands may be + * scheduled directly using {@link #schedule(Command)}, or be bound to {@link Trigger Triggers} to + * automatically handle scheduling and cancellation based on internal or external events. User code + * is responsible for calling {@link #run()} periodically to update trigger conditions and execute + * scheduled commands. Most often, this is done by overriding {@link TimedRobot#robotPeriodic()} to + * include a call to {@code Scheduler.getDefault().run()}: + * + *

{@code
+ * public class Robot extends TimedRobot {
+ *   @Override
+ *   public void robotPeriodic() {
+ *     // Update the scheduler on every loop
+ *     Scheduler.getDefault().run();
+ *   }
+ * }
+ * }
+ * + *

Danger

+ * + *

The scheduler must be used in a single-threaded program. Commands must be scheduled and + * canceled by the same thread that runs the scheduler, and cannot be run in a virtual thread. + * + *

Using the commands framework in a multithreaded environment can cause crashes in the + * Java virtual machine at any time, including on an official field during a match. The + * Java JIT compilers make assumptions that rely on coroutines being used on a single thread. + * Breaking those assumptions can cause incorrect JIT code to be generated with undefined behavior, + * potentially causing control issues or crashes deep in JIT-generated native code. + * + *

Normal concurrency constructs like locks, atomic references, and synchronized blocks + * or methods cannot save you. + * + *

Lifecycle

+ * + *

The {@link #run()} method runs five steps: + * + *

    + *
  1. Call {@link #sideload(Consumer) periodic sideload functions} + *
  2. Poll all registered triggers to queue and cancel commands + *
  3. Queue default commands for any mechanisms without a running command. The queued commands + * can be superseded by any manual scheduling or commands scheduled by triggers in the next + * run. + *
  4. Start all queued commands. This happens after all triggers are checked in case multiple + * commands with conflicting requirements are queued in the same update; the last command to + * be queued takes precedence over the rest. + *
  5. Loop over all running commands, mounting and calling each in turn until they either exit or + * call {@link Coroutine#yield()}. Commands run in the order in which they were scheduled. + *
+ * + *

Telemetry

+ * + *

There are two mechanisms for telemetry for a scheduler. A protobuf serializer can be used to + * take a snapshot of a scheduler instance, and report what commands are queued (scheduled but have + * not yet started to run), commands that are running (along with timing data for each command), and + * total time spent in the most recent {@link #run()} call. However, it cannot detect one-shot + * commands that are scheduled, run, and complete all in a single {@code run()} invocation - + * effectively, commands that never call {@link Coroutine#yield()} are invisible. + * + *

A second telemetry mechanism is provided by {@link #addEventListener(Consumer)}. The scheduler + * will issue events to all registered listeners when certain events occur (see {@link + * SchedulerEvent} for all event types). These events are emitted immediately and can be used to + * detect lifecycle events for all commands, including one-shots that would be invisible to the + * protobuf serializer. However, it is up to the user to log those events themselves. + */ +public final class Scheduler implements ProtobufSerializable { + private final Map m_defaultCommands = new LinkedHashMap<>(); + + /** The set of commands scheduled since the start of the previous run. */ + private final SequencedSet m_queuedToRun = new LinkedHashSet<>(); + + /** + * The states of all running commands (does not include on deck commands). We preserve insertion + * order to guarantee that child commands run after their parents. + */ + private final SequencedMap m_runningCommands = new LinkedHashMap<>(); + + /** + * The stack of currently executing commands. Child commands are pushed onto the stack and popped + * when they complete. Use {@link #currentState()} and {@link #currentCommand()} to get the + * currently executing command or its state. + */ + private final Stack m_currentCommandAncestry = new Stack<>(); + + /** The periodic callbacks to run, outside of the command structure. */ + private final List m_periodicCallbacks = new ArrayList<>(); + + /** Event loop for trigger bindings. */ + private final EventLoop m_eventLoop = new EventLoop(); + + /** The scope for continuations to yield to. */ + private final ContinuationScope m_scope = new ContinuationScope("coroutine commands"); + + // Telemetry + /** Protobuf serializer for a scheduler. */ + public static final SchedulerProto proto = new SchedulerProto(); + + private double m_lastRunTimeMs = -1; + + private final Set> m_eventListeners = new LinkedHashSet<>(); + + /** The default scheduler instance. */ + private static final Scheduler s_defaultScheduler = new Scheduler(); + + /** + * Gets the default scheduler instance for use in a robot program. Unless otherwise specified, + * triggers and mechanisms will be registered with the default scheduler and require the default + * scheduler to run. However, triggers and mechanisms can be constructed to be registered with a + * specific scheduler instance, which may be useful for isolation for unit tests. + * + * @return the default scheduler instance. + */ + @NoDiscard + public static Scheduler getDefault() { + return s_defaultScheduler; + } + + /** + * Creates a new scheduler object. Note that most built-in constructs like {@link Trigger} and + * {@link CommandGenericHID} will use the {@link #getDefault() default scheduler instance} unless + * they were explicitly constructed with a different scheduler instance. Teams should use the + * default instance for convenience; however, new scheduler instances can be useful for unit + * tests. + * + * @return a new scheduler instance that is independent of the default scheduler instance. + */ + @NoDiscard + public static Scheduler createIndependentScheduler() { + return new Scheduler(); + } + + /** Private constructor. Use static factory methods or the default scheduler instance. */ + private Scheduler() {} + + /** + * Sets the default command for a mechanism. The command must require that mechanism, and cannot + * require any other mechanisms. + * + * @param mechanism the mechanism for which to set the default command + * @param defaultCommand the default command to execute on the mechanism + * @throws IllegalArgumentException if the command does not meet the requirements for being a + * default command + */ + public void setDefaultCommand(Mechanism mechanism, Command defaultCommand) { + if (!defaultCommand.requires(mechanism)) { + throw new IllegalArgumentException( + "A mechanism's default command must require that mechanism"); + } + + if (defaultCommand.requirements().size() > 1) { + throw new IllegalArgumentException( + "A mechanism's default command cannot require other mechanisms"); + } + + m_defaultCommands.put(mechanism, defaultCommand); + } + + /** + * Gets the default command set for a mechanism. + * + * @param mechanism The mechanism + * @return The default command, or null if no default command was ever set + */ + public Command getDefaultCommandFor(Mechanism mechanism) { + return m_defaultCommands.get(mechanism); + } + + /** + * Adds a callback to run as part of the scheduler. The callback should not manipulate or control + * any mechanisms, but can be used to log information, update data (such as simulations or LED + * data buffers), or perform some other helpful task. The callback is responsible for managing its + * own control flow and end conditions. If you want to run a single task periodically for the + * entire lifespan of the scheduler, use {@link #addPeriodic(Runnable)}. + * + *

Note: Like commands, any loops in the callback must appropriately yield + * control back to the scheduler with {@link Coroutine#yield} or risk stalling your program in an + * unrecoverable infinite loop! + * + * @param callback the callback to sideload + */ + public void sideload(Consumer callback) { + var coroutine = new Coroutine(this, m_scope, callback); + m_periodicCallbacks.add(coroutine); + } + + /** + * Adds a task to run repeatedly for as long as the scheduler runs. This internally handles the + * looping and control yielding necessary for proper function. The callback will run at the same + * periodic frequency as the scheduler. + * + *

For example: + * + *

{@code
+   * scheduler.addPeriodic(() -> leds.setData(ledDataBuffer));
+   * scheduler.addPeriodic(() -> {
+   *   SmartDashboard.putNumber("X", getX());
+   *   SmartDashboard.putNumber("Y", getY());
+   * });
+   * }
+ * + * @param callback the periodic function to run + */ + public void addPeriodic(Runnable callback) { + sideload( + coroutine -> { + while (true) { + callback.run(); + coroutine.yield(); + } + }); + } + + /** Represents possible results of a command scheduling attempt. */ + public enum ScheduleResult { + /** The command was successfully scheduled and added to the queue. */ + SUCCESS, + /** The command is already scheduled or running. */ + ALREADY_RUNNING, + /** The command is a lower priority and conflicts with a command that's already running. */ + LOWER_PRIORITY_THAN_RUNNING_COMMAND, + } + + /** + * Schedules a command to run. If one command schedules another (a "parent" and "child"), the + * child command will be canceled when the parent command completes. It is not possible to fork a + * child command and have it live longer than its parent. + * + *

Does nothing if the command is already scheduled or running, or requires at least one + * mechanism already used by a higher priority command. + * + * @param command the command to schedule + * @return the result of the scheduling attempt. See {@link ScheduleResult} for details. + * @throws IllegalArgumentException if scheduled by a command composition that has already + * scheduled another command that shares at least one required mechanism + */ + // Implementation detail: a child command will immediately start running when scheduled by a + // parent command, skipping the queue entirely. This avoids dead loop cycles where a parent + // schedules a child, appending it to the queue, then waits for the next cycle to pick it up and + // start it. With deeply nested commands, dead loops could quickly to add up and cause the + // innermost commands that actually _do_ something to start running hundreds of milliseconds after + // their root ancestor was scheduled. + public ScheduleResult schedule(Command command) { + // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier + // stack frame filtering and modification. + var binding = + new Binding( + BindingScope.global(), BindingType.IMMEDIATE, command, new Throwable().getStackTrace()); + + return schedule(binding); + } + + // Package-private for use by Trigger + ScheduleResult schedule(Binding binding) { + var command = binding.command(); + + if (isScheduledOrRunning(command)) { + return ScheduleResult.ALREADY_RUNNING; + } + + if (lowerPriorityThanConflictingCommands(command)) { + return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND; + } + + for (var scheduledState : m_queuedToRun) { + if (!command.conflictsWith(scheduledState.command())) { + // No shared requirements, skip + continue; + } + if (command.isLowerPriorityThan(scheduledState.command())) { + // Lower priority than an already-scheduled (but not yet running) command that requires at + // one of the same mechanism. Ignore it. + return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND; + } + } + + // Evict conflicting on-deck commands + // We check above if the input command is lower priority than any of these, + // so at this point we're guaranteed to be >= priority than anything already on deck + evictConflictingOnDeckCommands(command); + + // If the binding is scoped to a particular command, that command is the parent. If we're in the + // middle of a run cycle and running commands, the parent is whatever command is currently + // running. Otherwise, there is no parent command. + var parentCommand = + binding.scope() instanceof BindingScope.ForCommand scope + ? scope.command() + : currentCommand(); + var state = new CommandState(command, parentCommand, buildCoroutine(command), binding); + + emitScheduledEvent(command); + + if (currentState() != null) { + // Scheduling a child command while running. Start it immediately instead of waiting a full + // cycle. This prevents issues with deeply nested command groups taking many scheduler cycles + // to start running the commands that actually _do_ things + evictConflictingRunningCommands(state); + m_runningCommands.put(command, state); + runCommand(state); + } else { + // Scheduling outside a command, add it to the pending set. If it's not overridden by another + // conflicting command being scheduled in the same scheduler loop, it'll be promoted and + // start to run when #runCommands() is called + m_queuedToRun.add(state); + } + + return ScheduleResult.SUCCESS; + } + + /** + * Checks if a command conflicts with and is a lower priority than any running command. Used when + * determining if the command can be scheduled. + */ + private boolean lowerPriorityThanConflictingCommands(Command command) { + Set ancestors = new HashSet<>(); + for (var state = currentState(); state != null; state = m_runningCommands.get(state.parent())) { + ancestors.add(state); + } + + // Check for conflicts with the commands that are already running + for (var state : m_runningCommands.values()) { + if (ancestors.contains(state)) { + // Can't conflict with an ancestor command + continue; + } + + var c = state.command(); + if (c.conflictsWith(command) && command.isLowerPriorityThan(c)) { + return true; + } + } + + return false; + } + + private void evictConflictingOnDeckCommands(Command command) { + for (var iterator = m_queuedToRun.iterator(); iterator.hasNext(); ) { + var scheduledState = iterator.next(); + var scheduledCommand = scheduledState.command(); + if (scheduledCommand.conflictsWith(command)) { + // Remove the lower priority conflicting command from the on deck commands. + // We don't need to call removeOrphanedChildren here because it hasn't started yet, + // meaning it hasn't had a chance to schedule any children + iterator.remove(); + emitInterruptedEvent(scheduledCommand, command); + emitCanceledEvent(scheduledCommand); + } + } + } + + /** + * Cancels all running commands with which an incoming state conflicts. Ancestor commands of the + * incoming state will not be canceled. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + private void evictConflictingRunningCommands(CommandState incomingState) { + // The set of root states with which the incoming state conflicts but does not inherit from + Set conflictingRootStates = + m_runningCommands.values().stream() + .filter(state -> incomingState.command().conflictsWith(state.command())) + .filter(state -> !isAncestorOf(state.command(), incomingState)) + .map( + state -> { + // Find the highest level ancestor of the conflicting command from which the + // incoming state does _not_ inherit. If they're totally unrelated, this will + // get the very root ancestor; otherwise, it'll return a direct sibling of the + // incoming command + CommandState root = state; + while (root.parent() != null && root.parent() != incomingState.parent()) { + root = m_runningCommands.get(root.parent()); + } + return root; + }) + .collect(Collectors.toSet()); + + // Cancel the root commands + for (var conflictingState : conflictingRootStates) { + emitInterruptedEvent(conflictingState.command(), incomingState.command()); + cancel(conflictingState.command()); + } + } + + /** + * Checks if a particular command is an ancestor of another. + * + * @param ancestor the potential ancestor for which to search + * @param state the state to check + * @return true if {@code ancestor} is the direct parent or indirect ancestor, false if not + */ + @SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.SimplifyBooleanReturns"}) + private boolean isAncestorOf(Command ancestor, CommandState state) { + if (state.parent() == null) { + // No parent, cannot inherit + return false; + } + if (!m_runningCommands.containsKey(ancestor)) { + // The given ancestor isn't running + return false; + } + if (state.parent() == ancestor) { + // Direct child + return true; + } + // Check if the command's parent inherits from the given ancestor + return m_runningCommands.values().stream() + .filter(s -> state.parent() == s.command()) + .anyMatch(s -> isAncestorOf(ancestor, s)); + } + + /** + * Cancels a command and any other command scheduled by it. This occurs immediately and does not + * need to wait for a call to {@link #run()}. Any command that it scheduled will also be canceled + * to ensure commands within compositions do not continue to run. + * + *

This has no effect if the given command is not currently scheduled or running. + * + * @param command the command to cancel + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public void cancel(Command command) { + if (command == currentCommand()) { + throw new IllegalArgumentException( + "Command `" + command.name() + "` is mounted and cannot be canceled"); + } + + boolean running = isRunning(command); + + // Evict the command. The next call to run() will schedule the default command for all its + // required mechanisms, unless another command requiring those mechanisms is scheduled between + // calling cancel() and calling run() + m_runningCommands.remove(command); + m_queuedToRun.removeIf(state -> state.command() == command); + + if (running) { + // Only run the hook if the command was running. If it was on deck or not + // even in the scheduler at the time, then there's nothing to do + command.onCancel(); + emitCanceledEvent(command); + } + + // Clean up any orphaned child commands; their lifespan must not exceed the parent's + removeOrphanedChildren(command); + } + + /** + * Updates the command scheduler. This will run operations in the following order: + * + *

    + *
  1. Run sideloaded functions from {@link #sideload(Consumer)} and {@link + * #addPeriodic(Runnable)} + *
  2. Update trigger bindings to queue and cancel bound commands + *
  3. Queue default commands for mechanisms that do not have a queued or running command + *
  4. Promote queued commands to the running set + *
  5. For every command in the running set, mount and run that command until it calls {@link + * Coroutine#yield()} or exits + *
+ * + *

This method is intended to be called in a periodic loop like {@link + * TimedRobot#robotPeriodic()} + */ + public void run() { + final long startMicros = RobotController.getTime(); + + // Sideloads may change some state that affects triggers. Run them first. + runPeriodicSideloads(); + + // Poll triggers next to schedule and cancel commands + m_eventLoop.poll(); + + // Schedule default commands for any mechanisms that don't have a running command and didn't + // have a new command scheduled by a sideload function or a trigger + scheduleDefaultCommands(); + + // Move all scheduled commands to the running set + promoteScheduledCommands(); + + // Run every command in order until they call Coroutine.yield() or exit + runCommands(); + + final long endMicros = RobotController.getTime(); + m_lastRunTimeMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds); + } + + private void promoteScheduledCommands() { + // Clear any commands that conflict with the scheduled set + for (var queuedState : m_queuedToRun) { + evictConflictingRunningCommands(queuedState); + } + + // Move any scheduled commands to the running set + for (var queuedState : m_queuedToRun) { + m_runningCommands.put(queuedState.command(), queuedState); + } + + // Clear the set of on-deck commands, + // since we just put them all into the set of running commands + m_queuedToRun.clear(); + } + + private void runPeriodicSideloads() { + // Update periodic callbacks + for (Coroutine coroutine : m_periodicCallbacks) { + coroutine.mount(); + try { + coroutine.runToYieldPoint(); + } finally { + Continuation.mountContinuation(null); + } + } + + // And remove any periodic callbacks that have completed + m_periodicCallbacks.removeIf(Coroutine::isDone); + } + + private void runCommands() { + // Tick every command that hasn't been completed yet + // Run in reverse so parent commands can resume in the same loop cycle an awaited child command + // completes. Otherwise, parents could only resume on the next loop cycle, introducing a delay + // at every layer of nesting. + for (var state : List.copyOf(m_runningCommands.values()).reversed()) { + runCommand(state); + } + } + + /** + * Mounts and runs a command until it yields or exits. + * + * @param state The command state to run + */ + @SuppressWarnings("PMD.AvoidCatchingGenericException") + private void runCommand(CommandState state) { + final var command = state.command(); + final var coroutine = state.coroutine(); + + if (!m_runningCommands.containsKey(command)) { + // Probably canceled by an owning composition, do not run + return; + } + + var previousState = currentState(); + + m_currentCommandAncestry.push(state); + long startMicros = RobotController.getTime(); + emitMountedEvent(command); + coroutine.mount(); + try { + coroutine.runToYieldPoint(); + } catch (RuntimeException e) { + // Command encountered an uncaught exception. + handleCommandException(state, e); + } finally { + long endMicros = RobotController.getTime(); + double elapsedMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds); + state.setLastRuntimeMs(elapsedMs); + + if (state.equals(currentState())) { + // Remove the command we just ran from the top of the stack + m_currentCommandAncestry.pop(); + } + + if (previousState != null) { + // Remount the parent command, if there is one + previousState.coroutine().mount(); + } else { + Continuation.mountContinuation(null); + } + } + + if (coroutine.isDone()) { + // Immediately check if the command has completed and remove any children commands. + // This prevents child commands from being executed one extra time in the run() loop + emitCompletedEvent(command); + m_runningCommands.remove(command); + removeOrphanedChildren(command); + } else { + // Yielded + emitYieldedEvent(command); + } + } + + /** + * Handles uncaught runtime exceptions from a mounted and running command. The command's ancestor + * and child commands will all be canceled and the exception's backtrace will be modified to + * include the stack frames of the schedule call site. + * + * @param state The state of the command that encountered the exception. + * @param e The exception that was thrown. + * @throws RuntimeException rethrows the exception, with a modified backtrace pointing to the + * schedule site + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + private void handleCommandException(CommandState state, RuntimeException e) { + var command = state.command(); + + // Fetch the root command + // (needs to be done before removing the failed command from the running set) + Command root = command; + while (getParentOf(root) != null) { + root = getParentOf(root); + } + + // Remove it from the running set. + m_runningCommands.remove(command); + + // Intercept the exception, inject stack frames from the schedule site, and rethrow it + var binding = state.binding(); + e.setStackTrace(CommandTraceHelper.modifyTrace(e.getStackTrace(), binding.frames())); + emitCompletedWithErrorEvent(command, e); + + // Clean up child commands after emitting the event so child Canceled events are emitted + // after the parent's CompletedWithError + removeOrphanedChildren(command); + + // Bubble up to the root and cancel all commands between the root and this one + // Note: Because we remove the command from the running set above, we still need to + // clean up all the failed command's children + if (root != null && root != command) { + cancel(root); + } + + // Then rethrow the exception + throw e; + } + + /** + * Gets the currently executing command state, or null if no command is currently executing. + * + * @return the currently executing command state + */ + private CommandState currentState() { + if (m_currentCommandAncestry.isEmpty()) { + // Avoid EmptyStackException + return null; + } + + return m_currentCommandAncestry.peek(); + } + + /** + * Gets the currently executing command, or null if no command is currently executing. + * + * @return the currently executing command + */ + public Command currentCommand() { + var state = currentState(); + if (state == null) { + return null; + } + + return state.command(); + } + + private void scheduleDefaultCommands() { + // Schedule the default commands for every mechanism that doesn't currently have a running or + // scheduled command. + m_defaultCommands.forEach( + (mechanism, defaultCommand) -> { + if (m_runningCommands.keySet().stream().noneMatch(c -> c.requires(mechanism)) + && m_queuedToRun.stream().noneMatch(c -> c.command().requires(mechanism)) + && defaultCommand != null) { + // Nothing currently running or scheduled + // Schedule the mechanism's default command, if it has one + schedule(defaultCommand); + } + }); + } + + /** + * Removes all commands descended from a parent command. This is used to ensure that any command + * scheduled within a composition or group cannot live longer than any ancestor. + * + * @param parent the root command whose descendants to remove from the scheduler + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + private void removeOrphanedChildren(Command parent) { + m_runningCommands.entrySet().stream() + .filter(e -> e.getValue().parent() == parent) + .toList() // copy to an intermediate list to avoid concurrent modification + .forEach(e -> cancel(e.getKey())); + } + + /** + * Builds a coroutine object that the command will be bound to. The coroutine will be scoped to + * this scheduler object and cannot be used by another scheduler instance. + * + * @param command the command for which to build a coroutine + * @return the binding coroutine + */ + private Coroutine buildCoroutine(Command command) { + return new Coroutine(this, m_scope, command::run); + } + + /** + * Checks if a command is currently running. + * + * @param command the command to check + * @return true if the command is running, false if not + */ + public boolean isRunning(Command command) { + return m_runningCommands.containsKey(command); + } + + /** + * Checks if a command is currently scheduled to run, but is not yet running. + * + * @param command the command to check + * @return true if the command is scheduled to run, false if not + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public boolean isScheduled(Command command) { + return m_queuedToRun.stream().anyMatch(state -> state.command() == command); + } + + /** + * Checks if a command is currently scheduled to run, or is already running. + * + * @param command the command to check + * @return true if the command is scheduled to run or is already running, false if not + */ + public boolean isScheduledOrRunning(Command command) { + return isScheduled(command) || isRunning(command); + } + + /** + * Gets the set of all currently running commands. Commands are returned in the order in which + * they were scheduled. The returned set is read-only. + * + * @return the currently running commands + */ + public Collection getRunningCommands() { + return Collections.unmodifiableSet(m_runningCommands.keySet()); + } + + /** + * Gets all the currently running commands that require a particular mechanism. Commands are + * returned in the order in which they were scheduled. The returned list is read-only. + * + * @param mechanism the mechanism to get the commands for + * @return the currently running commands that require the mechanism. + */ + public List getRunningCommandsFor(Mechanism mechanism) { + return m_runningCommands.keySet().stream() + .filter(command -> command.requires(mechanism)) + .toList(); + } + + /** + * Cancels all currently running and scheduled commands. All default commands will be scheduled on + * the next call to {@link #run()}, unless a higher priority command is scheduled or triggered + * after {@code cancelAll()} is used. + */ + public void cancelAll() { + for (var onDeckIter = m_queuedToRun.iterator(); onDeckIter.hasNext(); ) { + var state = onDeckIter.next(); + onDeckIter.remove(); + emitCanceledEvent(state.command()); + } + + for (var liveIter = m_runningCommands.entrySet().iterator(); liveIter.hasNext(); ) { + var entry = liveIter.next(); + liveIter.remove(); + Command canceledCommand = entry.getKey(); + canceledCommand.onCancel(); + emitCanceledEvent(canceledCommand); + } + } + + /** + * An event loop used by the scheduler to poll triggers that schedule or cancel commands. This + * event loop is always polled on every call to {@link #run()}. Custom event loops need to be + * bound to this one for synchronicity with the scheduler; however, they can always be polled + * manually before or after the call to {@link #run()}. + * + * @return the default event loop. + */ + @NoDiscard + public EventLoop getDefaultEventLoop() { + return m_eventLoop; + } + + /** + * For internal use. + * + * @return The commands that have been scheduled but not yet started. + */ + @NoDiscard + public Collection getQueuedCommands() { + return m_queuedToRun.stream().map(CommandState::command).toList(); + } + + /** + * For internal use. + * + * @param command The command to check + * @return The command that forked the provided command. Null if the command is not a child of + * another command. + */ + public Command getParentOf(Command command) { + var state = m_runningCommands.get(command); + if (state == null) { + return null; + } + return state.parent(); + } + + /** + * Gets how long a command took to run in the previous cycle. If the command is not currently + * running, returns -1. + * + * @param command The command to check + * @return How long, in milliseconds, the command last took to execute. + */ + public double lastCommandRuntimeMs(Command command) { + if (m_runningCommands.containsKey(command)) { + return m_runningCommands.get(command).lastRuntimeMs(); + } else { + return -1; + } + } + + /** + * Gets how long a command has taken to run, in aggregate, since it was most recently scheduled. + * If the command is not currently running, returns -1. + * + * @param command The command to check + * @return How long, in milliseconds, the command has taken to execute in total + */ + public double totalRuntimeMs(Command command) { + if (m_runningCommands.containsKey(command)) { + return m_runningCommands.get(command).totalRuntimeMs(); + } else { + // Not running; no data + return -1; + } + } + + /** + * Gets the unique run id for a scheduled or running command. If the command is not currently + * scheduled or running, this function returns {@code 0}. + * + * @param command The command to get the run ID for + * @return The run of the command + */ + @NoDiscard + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public int runId(Command command) { + if (m_runningCommands.containsKey(command)) { + return m_runningCommands.get(command).id(); + } + + // Check scheduled commands + for (var scheduled : m_queuedToRun) { + if (scheduled.command() == command) { + return scheduled.id(); + } + } + + return 0; + } + + /** + * Gets how long the scheduler took to process its most recent {@link #run()} invocation, in + * milliseconds. Defaults to -1 if the scheduler has not yet run. + * + * @return How long, in milliseconds, the scheduler last took to execute. + */ + @NoDiscard + public double lastRuntimeMs() { + return m_lastRunTimeMs; + } + + // Event-base telemetry and helpers. The static factories are for convenience to automatically + // set the timestamp instead of littering RobotController.getTime() everywhere. + + private void emitScheduledEvent(Command command) { + var event = new SchedulerEvent.Scheduled(command, RobotController.getTime()); + emitEvent(event); + } + + private void emitMountedEvent(Command command) { + var event = new SchedulerEvent.Mounted(command, RobotController.getTime()); + emitEvent(event); + } + + private void emitYieldedEvent(Command command) { + var event = new SchedulerEvent.Yielded(command, RobotController.getTime()); + emitEvent(event); + } + + private void emitCompletedEvent(Command command) { + var event = new SchedulerEvent.Completed(command, RobotController.getTime()); + emitEvent(event); + } + + private void emitCompletedWithErrorEvent(Command command, Throwable error) { + var event = new SchedulerEvent.CompletedWithError(command, error, RobotController.getTime()); + emitEvent(event); + } + + private void emitCanceledEvent(Command command) { + var event = new SchedulerEvent.Canceled(command, RobotController.getTime()); + emitEvent(event); + } + + private void emitInterruptedEvent(Command command, Command interrupter) { + var event = new SchedulerEvent.Interrupted(command, interrupter, RobotController.getTime()); + emitEvent(event); + } + + /** + * Adds a listener to handle events that are emitted by the scheduler. Events are emitted when + * certain actions are taken by user code or by internal processing logic in the scheduler. + * Listeners should take care to be quick, simple, and not schedule or cancel commands, as that + * may cause inconsistent scheduler behavior or even cause a program crash. + * + *

Listeners are primarily expected to be for data logging and telemetry. In particular, a + * one-shot command (one that never calls {@link Coroutine#yield()}) will never appear in the + * standard protobuf telemetry because it is scheduled, runs, and finishes all in a single + * scheduler cycle. However, {@link SchedulerEvent.Scheduled},{@link SchedulerEvent.Mounted}, and + * {@link SchedulerEvent.Completed} events will be emitted corresponding to those actions, and + * user code can listen for and log such events. + * + * @param listener The listener to add. Cannot be null. + * @throws NullPointerException if given a null listener + */ + public void addEventListener(Consumer listener) { + ErrorMessages.requireNonNullParam(listener, "listener", "addEventListener"); + + m_eventListeners.add(listener); + } + + private void emitEvent(SchedulerEvent event) { + // TODO: Prevent listeners from interacting with the scheduler. + // Scheduling or canceling commands while the scheduler is processing will probably cause + // bugs in user code or even a program crash. + for (var listener : m_eventListeners) { + listener.accept(event); + } + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/SchedulerEvent.java b/commandsv3/src/main/java/org/wpilib/commands3/SchedulerEvent.java new file mode 100644 index 0000000000..32641722bb --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/SchedulerEvent.java @@ -0,0 +1,89 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import edu.wpi.first.wpilibj.RobotController; +import java.util.function.Consumer; + +/** + * An event that occurs during scheduler processing. This can range from {@link Scheduled a command + * being scheduled} by a trigger or manual call to {@link Scheduler#schedule(Command)} to {@link + * Interrupted a command being interrupted by another}. Event listeners can be registered with a + * scheduler via {@link Scheduler#addEventListener(Consumer)}. All events have a timestamp to + * indicate when the event occurred. + */ +public sealed interface SchedulerEvent { + /** + * The timestamp for when the event occurred. Measured in microseconds since some arbitrary start + * time. + * + * @return The event timestamp. + * @see RobotController#getTime() + */ + long timestampMicros(); + + /** + * An event marking when a command is scheduled in {@link Scheduler#schedule(Command)}. + * + * @param command The command that was scheduled + * @param timestampMicros When the command was scheduled + */ + record Scheduled(Command command, long timestampMicros) implements SchedulerEvent {} + + /** + * An event marking when a command starts running. + * + * @param command The command that started + * @param timestampMicros When the command started + */ + record Mounted(Command command, long timestampMicros) implements SchedulerEvent {} + + /** + * An event marking when a command yielded control with {@link Coroutine#yield()}. + * + * @param command The command that yielded + * @param timestampMicros When the command yielded + */ + record Yielded(Command command, long timestampMicros) implements SchedulerEvent {} + + /** + * An event marking when a command completed naturally. + * + * @param command The command that completed + * @param timestampMicros When the command completed + */ + record Completed(Command command, long timestampMicros) implements SchedulerEvent {} + + /** + * An event marking when a command threw or encountered an unhanded exception. + * + * @param command The command that encountered the error + * @param error The unhandled error + * @param timestampMicros When the error occurred + */ + record CompletedWithError(Command command, Throwable error, long timestampMicros) + implements SchedulerEvent {} + + /** + * An event marking when a command was canceled. If the command was canceled because it was + * interrupted by another command, an {@link Interrupted} will be emitted immediately prior to the + * cancellation event. + * + * @param command The command that was canceled + * @param timestampMicros When the command was canceled + */ + record Canceled(Command command, long timestampMicros) implements SchedulerEvent {} + + /** + * An event marking when a command was interrupted by another. Typically followed by an {@link + * Canceled} event. + * + * @param command The command that was interrupted + * @param interrupter The interrupting command + * @param timestampMicros When the command was interrupted + */ + record Interrupted(Command command, Command interrupter, long timestampMicros) + implements SchedulerEvent {} +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/SequentialGroup.java b/commandsv3/src/main/java/org/wpilib/commands3/SequentialGroup.java new file mode 100644 index 0000000000..e6458527a1 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/SequentialGroup.java @@ -0,0 +1,80 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A sequence of commands that run one after another. Each successive command only starts after its + * predecessor completes execution. The priority of a sequence is equal to the highest priority of + * any of its commands. If all commands in the sequence are able to run while the robot is disabled, + * then the sequence itself will be allowed to run while the robot is disabled. + * + *

The sequence will own all mechanisms required by all commands in the sequence for the + * entire duration of the sequence. This means that a mechanism owned by one command in the + * sequence, but not by a later one, will be uncommanded while that later command executes. + */ +public final class SequentialGroup implements Command { + private final String m_name; + private final List m_commands = new ArrayList<>(); + private final Set m_requirements = new HashSet<>(); + private final int m_priority; + + /** + * Creates a new command sequence. + * + * @param name the name of the sequence + * @param commands the commands to execute within the sequence + */ + SequentialGroup(String name, List commands) { + requireNonNullParam(name, "name", "SequentialGroup"); + requireNonNullParam(commands, "commands", "SequentialGroup"); + for (int i = 0; i < commands.size(); i++) { + requireNonNullParam(commands.get(i), "commands[" + i + "]", "SequentialGroup"); + } + + m_name = name; + m_commands.addAll(commands); + + for (var command : commands) { + m_requirements.addAll(command.requirements()); + } + + m_priority = + commands.stream().mapToInt(Command::priority).max().orElse(Command.DEFAULT_PRIORITY); + } + + @Override + public void run(Coroutine coroutine) { + for (var command : m_commands) { + coroutine.await(command); + } + } + + @Override + public String name() { + return m_name; + } + + @Override + public Set requirements() { + return m_requirements; + } + + @Override + public int priority() { + return m_priority; + } + + @Override + public String toString() { + return "SequentialGroup[name=" + m_name + "]"; + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/SequentialGroupBuilder.java b/commandsv3/src/main/java/org/wpilib/commands3/SequentialGroupBuilder.java new file mode 100644 index 0000000000..5c10a61aa4 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/SequentialGroupBuilder.java @@ -0,0 +1,106 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; +import org.wpilib.annotation.NoDiscard; + +/** + * A builder class to configure and then create a {@link SequentialGroup}. Like {@link + * StagedCommandBuilder}, the final command is created by calling the terminal {@link + * #named(String)} method, or with an automatically generated name using {@link + * #withAutomaticName()}. + */ +@NoDiscard +public class SequentialGroupBuilder { + private final List m_steps = new ArrayList<>(); + private BooleanSupplier m_endCondition; + + /** + * Creates new SequentialGroupBuilder. The builder will have no commands and have no preapplied + * configuration options. Use {@link #andThen(Command)} to add commands to the sequence. + */ + public SequentialGroupBuilder() {} + + /** + * Adds a command to the sequence. + * + * @param next The next command in the sequence + * @return The builder object, for chaining + */ + public SequentialGroupBuilder andThen(Command next) { + requireNonNullParam(next, "next", "SequentialGroupBuilder.andThen"); + + m_steps.add(next); + return this; + } + + /** + * Adds commands to the sequence. Commands will be added to the sequence in the order they are + * passed to this method. + * + * @param nextCommands The next commands in the sequence + * @return The builder object, for chaining + */ + public SequentialGroupBuilder andThen(Command... nextCommands) { + requireNonNullParam(nextCommands, "nextCommands", "SequentialGroupBuilder.andThen"); + for (int i = 0; i < nextCommands.length; i++) { + requireNonNullParam( + nextCommands[i], "nextCommands[" + i + "]", "SequentialGroupBuilder.andThen"); + } + + m_steps.addAll(Arrays.asList(nextCommands)); + return this; + } + + /** + * Adds an end condition to the command group. If this condition is met before all required + * commands have completed, the group will exit early. If multiple end conditions are added (e.g. + * {@code .until(() -> conditionA()).until(() -> conditionB())}), then the last end condition + * added will be used and any previously configured condition will be overridden. + * + * @param endCondition The end condition for the group + * @return The builder object, for chaining + */ + public SequentialGroupBuilder until(BooleanSupplier endCondition) { + m_endCondition = endCondition; + return this; + } + + /** + * Creates the sequence command, giving it the specified name. + * + * @param name The name of the sequence command + * @return The built command + */ + public Command named(String name) { + var seq = new SequentialGroup(name, m_steps); + if (m_endCondition != null) { + // No custom end condition, return the group as is + return seq; + } + + // We have a custom end condition, so we need to wrap the group in a race + return new ParallelGroupBuilder() + .optional(seq, Command.waitUntil(m_endCondition).named("Until Condition")) + .named(name); + } + + /** + * Creates the sequence command, giving it an automatically generated name based on the commands + * in the sequence. + * + * @return The built command + */ + public Command withAutomaticName() { + return named(m_steps.stream().map(Command::name).collect(Collectors.joining(" -> "))); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/StagedCommandBuilder.java b/commandsv3/src/main/java/org/wpilib/commands3/StagedCommandBuilder.java new file mode 100644 index 0000000000..5b68ce5602 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/StagedCommandBuilder.java @@ -0,0 +1,299 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import org.wpilib.annotation.NoDiscard; + +/** + * A builder class for commands. Command configuration is done in stages, where later stages have + * different configuration options than earlier stages. Commands may only be created after going + * through every stage, enforcing the presence of required options. All commands must have a + * set of requirements (which may be empty), a name, and an implementation. The builder stages are + * defined such that the final stage that creates a command can only be reached after going through + * the earlier stages to configure those required options. + * + *

Example usage: + * + *

{@code
+ * StagedCommandBuilder start = new StagedCommandBuilder();
+ * NeedsExecutionBuilderStage withRequirements = start.requiring(mechanism1, mechanism2);
+ * NeedsNameBuilderStage withExecution = withRequirements.executing(coroutine -> ...);
+ * Command exampleCommand = withExecution.named("Example Command");
+ * }
+ * + *

Because every method on the builders returns a builder object, these calls can be chained to + * cut down on verbosity and make the code easier to read. This is the recommended style: + * + *

{@code
+ * Command exampleCommand =
+ *   new StagedCommandBuilder()
+ *     .requiring(mechanism1, mechanism2)
+ *     .executing(coroutine -> ...)
+ *     .named("Example Command");
+ * }
+ * + *

And can be cut down even further by using helper methods provided by the library: + * + *

{@code
+ * Command exampleCommand =
+ *   Command
+ *     .requiring(mechanism1, mechanism2)
+ *     .executing(coroutine -> ...)
+ *     .named("Example Command");
+ * }
+ */ +@NoDiscard +public final class StagedCommandBuilder { + private final Set m_requirements = new HashSet<>(); + private Consumer m_impl; + private Runnable m_onCancel = () -> {}; + private String m_name; + private int m_priority = Command.DEFAULT_PRIORITY; + private BooleanSupplier m_endCondition; + + private Command m_builtCommand; + + // Using internal anonymous classes to implement the staged builder APIs, but all backed by the + // state of the enclosing StagedCommandBuilder object + + private final NeedsExecutionBuilderStage m_needsExecutionView = + new NeedsExecutionBuilderStage() { + @Override + public NeedsExecutionBuilderStage requiring(Mechanism requirement) { + throwIfAlreadyBuilt(); + + requireNonNullParam(requirement, "requirement", "StagedCommandBuilder.requiring"); + m_requirements.add(requirement); + return this; + } + + @Override + public NeedsExecutionBuilderStage requiring(Mechanism requirement, Mechanism... extra) { + throwIfAlreadyBuilt(); + + requireNonNullParam(requirement, "requirement", "StagedCommandBuilder.requiring"); + requireNonNullParam(extra, "extra", "StagedCommandBuilder.requiring"); + + for (int i = 0; i < extra.length; i++) { + requireNonNullParam(extra[i], "extra[" + i + "]", "StagedCommandBuilder.requiring"); + } + + m_requirements.add(requirement); + m_requirements.addAll(Arrays.asList(extra)); + return this; + } + + @Override + public NeedsExecutionBuilderStage requiring(Collection requirements) { + throwIfAlreadyBuilt(); + + requireNonNullParam(requirements, "requirements", "StagedCommandBuilder.requiring"); + int i = 0; + for (Mechanism requirement : requirements) { + requireNonNullParam( + requirement, "requirements[" + i + "]", "StagedCommandBuilder.requiring"); + i++; + } + + m_requirements.addAll(requirements); + return this; + } + + @Override + public NeedsNameBuilderStage executing(Consumer impl) { + throwIfAlreadyBuilt(); + + requireNonNullParam(impl, "impl", "StagedCommandBuilder.executing"); + m_impl = impl; + return m_needsNameView; + } + }; + + private final NeedsNameBuilderStage m_needsNameView = + new NeedsNameBuilderStage() { + @Override + public NeedsNameBuilderStage whenCanceled(Runnable onCancel) { + throwIfAlreadyBuilt(); + + m_onCancel = onCancel; + return this; + } + + @Override + public NeedsNameBuilderStage withPriority(int priority) { + throwIfAlreadyBuilt(); + + m_priority = priority; + return this; + } + + @Override + public NeedsNameBuilderStage until(BooleanSupplier endCondition) { + throwIfAlreadyBuilt(); + + m_endCondition = endCondition; // allowed to be null + return this; + } + + @Override + public Command named(String name) { + throwIfAlreadyBuilt(); + + requireNonNullParam(name, "name", "StagedCommandBuilder.withName"); + m_name = name; + + var command = new BuilderBackedCommand(StagedCommandBuilder.this); + + if (m_endCondition == null) { + // No custom end condition, just return the raw command + m_builtCommand = command; + } else { + // A custom end condition is implemented as a race group, since we cannot modify the + // command body to inject the end condition. + m_builtCommand = + new ParallelGroupBuilder().requiring(command).until(m_endCondition).named(m_name); + } + + return m_builtCommand; + } + }; + + private static final class BuilderBackedCommand implements Command { + private static final Runnable kNoOp = () -> {}; + + private final Set m_requirements; + private final Consumer m_impl; + private final Runnable m_onCancel; + private final String m_name; + private final int m_priority; + + private BuilderBackedCommand(StagedCommandBuilder builder) { + // Copy builder fields into the command so the builder object can be garbage collected + m_requirements = new HashSet<>(builder.m_requirements); + m_impl = builder.m_impl; + m_onCancel = Objects.requireNonNullElse(builder.m_onCancel, kNoOp); + m_name = builder.m_name; + m_priority = builder.m_priority; + } + + @Override + public void run(Coroutine coroutine) { + m_impl.accept(coroutine); + } + + @Override + public void onCancel() { + m_onCancel.run(); + } + + @Override + public String name() { + return m_name; + } + + @Override + public Set requirements() { + return m_requirements; + } + + @Override + public int priority() { + return m_priority; + } + + @Override + public String toString() { + return name(); + } + } + + /** + * Creates a new command builder. All required options must be set on each stage before a command + * is able to be created. Attempting to create a command without setting all required options will + * result in a compilation error. + */ + public StagedCommandBuilder() {} + + /** + * Explicitly marks the command as requiring no mechanisms. Unless overridden later with {@link + * NeedsExecutionBuilderStage#requiring(Mechanism)} or a similar method, the built command will + * not have ownership over any mechanisms when it runs. Use this for commands that don't need to + * own a mechanism, such as a gyro zeroing command, that does some kind of cleanup task without + * needing to control something. + * + * @return A builder object that can be used to further configure the command. + */ + public NeedsExecutionBuilderStage noRequirements() { + throwIfAlreadyBuilt(); + + return m_needsExecutionView; + } + + /** + * Marks the command as requiring one or more mechanisms. If only a single mechanism is required, + * prefer a factory function like {@link Mechanism#run(Consumer)} or similar - it will + * automatically require the mechanism, instead of it needing to be explicitly specified. + * + * @param requirement The first required mechanism. Cannot be null. + * @param extra Any optional extra required mechanisms. May be empty, but cannot be null or + * contain null values. + * @return A builder object that can be used to further configure the command. + */ + public NeedsExecutionBuilderStage requiring(Mechanism requirement, Mechanism... extra) { + throwIfAlreadyBuilt(); + + requireNonNullParam(requirement, "requirement", "StagedCommandBuilder.requiring"); + requireNonNullParam(extra, "extra", "StagedCommandBuilder.requiring"); + + for (int i = 0; i < extra.length; i++) { + requireNonNullParam(extra[i], "extra[" + i + "]", "StagedCommandBuilder.requiring"); + } + + m_requirements.add(requirement); + m_requirements.addAll(Arrays.asList(extra)); + return m_needsExecutionView; + } + + /** + * Marks the command as requiring zero or more mechanisms. If only a single mechanism is required, + * prefer a factory function like {@link Mechanism#run(Consumer)} or similar - it will + * automatically require the mechanism, instead of it needing to be explicitly specified. + * + * @param requirements A collection of required mechanisms. May be empty, but cannot be null or + * contain null values. + * @return A builder object that can be used to further configure the command. + */ + public NeedsExecutionBuilderStage requiring(Collection requirements) { + throwIfAlreadyBuilt(); + + requireNonNullParam(requirements, "requirements", "StagedCommandBuilder.requiring"); + int i = 0; + for (var mechanism : requirements) { + requireNonNullParam(mechanism, "requirements[" + i + "]", "StagedCommandBuilder.requiring"); + i++; + } + + m_requirements.addAll(requirements); + return m_needsExecutionView; + } + + // Prevent builders from being mutated after command creation + // Weird things could happen like changing requirements, priority level, or even the command + // implementation itself if we didn't prohibit it. + private void throwIfAlreadyBuilt() { + if (m_builtCommand != null) { + throw new IllegalStateException("Command builders cannot be reused"); + } + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/Trigger.java b/commandsv3/src/main/java/org/wpilib/commands3/Trigger.java new file mode 100644 index 0000000000..d9e273f029 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/Trigger.java @@ -0,0 +1,368 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.units.Units.Seconds; +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import edu.wpi.first.math.filter.Debouncer; +import edu.wpi.first.units.measure.Time; +import edu.wpi.first.wpilibj.event.EventLoop; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.function.BooleanSupplier; + +/** + * Triggers allow users to specify conditions for when commands should run. Triggers can be set up + * to read from joystick and controller buttons (eg {@link + * org.wpilib.commands3.button.CommandXboxController#x()}) or be customized to read sensor values or + * any other arbitrary true/false condition. + * + *

It is very easy to link a button to a command. For instance, you could link the trigger button + * of a joystick to a "score" command. + * + *

Triggers can easily be composed for advanced functionality using the {@link + * #and(BooleanSupplier)}, {@link #or(BooleanSupplier)}, {@link #negate()} operators. + * + *

Trigger bindings created inside a running command will only be active while that command is + * running. This is useful for defining trigger-based behavior only in a certain scope and avoids + * needing to create dozens of global triggers. Any commands scheduled by these triggers will be + * canceled when the enclosing command exits. + * + *

{@code
+ * Command shootWhileAiming = Command.noRequirements().executing(co -> {
+ *   turret.atTarget.onTrue(shooter.shootOnce());
+ *   co.await(turret.lockOnGoal());
+ * }).named("Shoot While Aiming");
+ * controller.rightBumper().whileTrue(shootWhileAiming);
+ * }
+ */ +public class Trigger implements BooleanSupplier { + private final BooleanSupplier m_condition; + private final EventLoop m_loop; + private final Scheduler m_scheduler; + private Signal m_previousSignal; + private final Map> m_bindings = new EnumMap<>(BindingType.class); + private final Runnable m_eventLoopCallback = this::poll; + private boolean m_isBoundToEventLoop; // used for lazily binding to the event loop + + /** + * Represents the state of a signal: high or low. Used instead of a boolean for nullity on the + * first run, when the previous signal value is undefined and unknown. + */ + private enum Signal { + /** The signal is high. */ + HIGH, + /** The signal is low. */ + LOW + } + + /** + * Creates a new trigger based on the given condition. Polled by the scheduler's {@link + * Scheduler#getDefaultEventLoop() default event loop}. + * + * @param scheduler The scheduler that should execute triggered commands. + * @param condition the condition represented by this trigger + */ + public Trigger(Scheduler scheduler, BooleanSupplier condition) { + m_scheduler = requireNonNullParam(scheduler, "scheduler", "Trigger"); + m_loop = scheduler.getDefaultEventLoop(); + m_condition = requireNonNullParam(condition, "condition", "Trigger"); + } + + /** + * Creates a new trigger based on the given condition. Triggered commands are executed by the + * {@link Scheduler#getDefault() default scheduler}. + * + *

Polled by the default scheduler button loop. + * + * @param condition the condition represented by this trigger + */ + public Trigger(BooleanSupplier condition) { + this(Scheduler.getDefault(), condition); + } + + /** + * Creates a new trigger based on the given condition. + * + * @param scheduler The scheduler that should execute triggered commands. + * @param loop The event loop to poll the trigger. + * @param condition the condition represented by this trigger + */ + public Trigger(Scheduler scheduler, EventLoop loop, BooleanSupplier condition) { + m_scheduler = requireNonNullParam(scheduler, "scheduler", "Trigger"); + m_loop = requireNonNullParam(loop, "loop", "Trigger"); + m_condition = requireNonNullParam(condition, "condition", "Trigger"); + } + + /** + * Starts the given command whenever the condition changes from `false` to `true`. + * + * @param command the command to start + * @return this trigger, so calls can be chained + */ + public Trigger onTrue(Command command) { + requireNonNullParam(command, "command", "onTrue"); + addBinding(BindingType.SCHEDULE_ON_RISING_EDGE, command); + return this; + } + + /** + * Starts the given command whenever the condition changes from `true` to `false`. + * + * @param command the command to start + * @return this trigger, so calls can be chained + */ + public Trigger onFalse(Command command) { + requireNonNullParam(command, "command", "onFalse"); + addBinding(BindingType.SCHEDULE_ON_FALLING_EDGE, command); + return this; + } + + /** + * Starts the given command when the condition changes to `true` and cancels it when the condition + * changes to `false`. + * + *

Doesn't re-start the command if it ends while the condition is still `true`. + * + * @param command the command to start + * @return this trigger, so calls can be chained + */ + public Trigger whileTrue(Command command) { + requireNonNullParam(command, "command", "whileTrue"); + addBinding(BindingType.RUN_WHILE_HIGH, command); + return this; + } + + /** + * Starts the given command when the condition changes to `false` and cancels it when the + * condition changes to `true`. + * + *

Doesn't re-start the command if it ends while the condition is still `false`. + * + * @param command the command to start + * @return this trigger, so calls can be chained + */ + public Trigger whileFalse(Command command) { + requireNonNullParam(command, "command", "whileFalse"); + addBinding(BindingType.RUN_WHILE_LOW, command); + return this; + } + + /** + * Toggles a command when the condition changes from `false` to `true`. + * + * @param command the command to toggle + * @return this trigger, so calls can be chained + */ + public Trigger toggleOnTrue(Command command) { + requireNonNullParam(command, "command", "toggleOnTrue"); + addBinding(BindingType.TOGGLE_ON_RISING_EDGE, command); + return this; + } + + /** + * Toggles a command when the condition changes from `true` to `false`. + * + * @param command the command to toggle + * @return this trigger, so calls can be chained + */ + public Trigger toggleOnFalse(Command command) { + requireNonNullParam(command, "command", "toggleOnFalse"); + addBinding(BindingType.TOGGLE_ON_FALLING_EDGE, command); + return this; + } + + @Override + public boolean getAsBoolean() { + return m_condition.getAsBoolean(); + } + + /** + * Composes two triggers with logical AND. + * + * @param trigger the condition to compose with + * @return A trigger which is active when both component triggers are active. + */ + public Trigger and(BooleanSupplier trigger) { + return new Trigger( + m_scheduler, m_loop, () -> m_condition.getAsBoolean() && trigger.getAsBoolean()); + } + + /** + * Composes two triggers with logical OR. + * + * @param trigger the condition to compose with + * @return A trigger which is active when either component trigger is active. + */ + public Trigger or(BooleanSupplier trigger) { + return new Trigger( + m_scheduler, m_loop, () -> m_condition.getAsBoolean() || trigger.getAsBoolean()); + } + + /** + * Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the + * negation of this trigger. + * + * @return the negated trigger + */ + public Trigger negate() { + return new Trigger(m_scheduler, m_loop, () -> !m_condition.getAsBoolean()); + } + + /** + * Creates a new debounced trigger from this trigger - it will become active when this trigger has + * been active for longer than the specified period. + * + * @param duration The debounce period. + * @return The debounced trigger (rising edges debounced only) + */ + public Trigger debounce(Time duration) { + return debounce(duration, Debouncer.DebounceType.kRising); + } + + /** + * Creates a new debounced trigger from this trigger - it will become active when this trigger has + * been active for longer than the specified period. + * + * @param duration The debounce period. + * @param type The debounce type. + * @return The debounced trigger. + */ + public Trigger debounce(Time duration, Debouncer.DebounceType type) { + var debouncer = new Debouncer(duration.in(Seconds), type); + return new Trigger(m_scheduler, m_loop, () -> debouncer.calculate(m_condition.getAsBoolean())); + } + + private void poll() { + // Clear bindings that no longer need to run + // This should always be checked, regardless of signal change, since bindings may be scoped + // and those scopes may become inactive. + clearStaleBindings(); + + var signal = readSignal(); + + if (signal == m_previousSignal) { + // No change in the signal. Nothing to do + return; + } + + if (signal == Signal.HIGH) { + // Signal is now high when it wasn't before - a rising edge + scheduleBindings(BindingType.SCHEDULE_ON_RISING_EDGE); + scheduleBindings(BindingType.RUN_WHILE_HIGH); + cancelBindings(BindingType.RUN_WHILE_LOW); + toggleBindings(BindingType.TOGGLE_ON_RISING_EDGE); + } + + if (signal == Signal.LOW) { + // Signal is now low when it wasn't before - a falling edge + scheduleBindings(BindingType.SCHEDULE_ON_FALLING_EDGE); + scheduleBindings(BindingType.RUN_WHILE_LOW); + cancelBindings(BindingType.RUN_WHILE_HIGH); + toggleBindings(BindingType.TOGGLE_ON_FALLING_EDGE); + } + + m_previousSignal = signal; + } + + private Signal readSignal() { + if (m_condition.getAsBoolean()) { + return Signal.HIGH; + } else { + return Signal.LOW; + } + } + + /** Removes bindings in inactive scopes. Running commands tied to those bindings are canceled. */ + private void clearStaleBindings() { + m_bindings.forEach( + (_bindingType, bindings) -> { + for (var iterator = bindings.iterator(); iterator.hasNext(); ) { + var binding = iterator.next(); + if (binding.scope().active()) { + // This binding's scope is still active, leave it running + continue; + } + + // The scope is no long active. Remove the binding and immediately cancel its command. + iterator.remove(); + m_scheduler.cancel(binding.command()); + } + }); + } + + /** + * Schedules all commands bound to the given binding type. + * + * @param bindingType the binding type to schedule + */ + private void scheduleBindings(BindingType bindingType) { + m_bindings.getOrDefault(bindingType, List.of()).forEach(m_scheduler::schedule); + } + + /** + * Cancels all commands bound to the given binding type. + * + * @param bindingType the binding type to cancel + */ + private void cancelBindings(BindingType bindingType) { + m_bindings + .getOrDefault(bindingType, List.of()) + .forEach(binding -> m_scheduler.cancel(binding.command())); + } + + /** + * Toggles all commands bound to the given binding type. If a command is currently scheduled or + * running, it will be canceled; otherwise, it will be scheduled. + * + * @param bindingType the binding type to cancel + */ + private void toggleBindings(BindingType bindingType) { + m_bindings + .getOrDefault(bindingType, List.of()) + .forEach( + binding -> { + var command = binding.command(); + if (m_scheduler.isScheduledOrRunning(command)) { + m_scheduler.cancel(command); + } else { + m_scheduler.schedule(binding); + } + }); + } + + // package-private for testing + void addBinding(BindingScope scope, BindingType bindingType, Command command) { + // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier + // stack frame filtering and modification. + m_bindings + .computeIfAbsent(bindingType, _k -> new ArrayList<>()) + .add(new Binding(scope, bindingType, command, new Throwable().getStackTrace())); + + if (!m_isBoundToEventLoop) { + m_loop.bind(m_eventLoopCallback); + m_isBoundToEventLoop = true; + } + } + + private void addBinding(BindingType bindingType, Command command) { + BindingScope scope = + switch (m_scheduler.currentCommand()) { + case Command c -> { + // A command is creating a binding - make it scoped to that specific command + yield BindingScope.forCommand(m_scheduler, c); + } + case null -> { + // Creating a binding outside a command - it's global in scope + yield BindingScope.global(); + } + }; + + addBinding(scope, bindingType, command); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/button/CommandGenericHID.java b/commandsv3/src/main/java/org/wpilib/commands3/button/CommandGenericHID.java new file mode 100644 index 0000000000..b3e7a710fc --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/button/CommandGenericHID.java @@ -0,0 +1,343 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.button; + +import edu.wpi.first.math.Pair; +import edu.wpi.first.wpilibj.DriverStation.POVDirection; +import edu.wpi.first.wpilibj.GenericHID; +import edu.wpi.first.wpilibj.event.EventLoop; +import java.util.HashMap; +import java.util.Map; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.Trigger; + +/** + * A version of {@link GenericHID} with {@link Trigger} factories for command-based. + * + * @see GenericHID + */ +public class CommandGenericHID { + private final Scheduler m_scheduler; + private final GenericHID m_hid; + private final Map> m_buttonCache = new HashMap<>(); + private final Map, Trigger>> m_axisLessThanCache = + new HashMap<>(); + private final Map, Trigger>> m_axisGreaterThanCache = + new HashMap<>(); + private final Map, Trigger>> + m_axisMagnitudeGreaterThanCache = new HashMap<>(); + private final Map> m_povCache = new HashMap<>(); + + /** + * Construct an instance of a device. + * + * @param scheduler The scheduler that should execute the triggered commands. + * @param port The port index on the Driver Station that the device is plugged into. + */ + public CommandGenericHID(Scheduler scheduler, int port) { + m_scheduler = scheduler; + m_hid = new GenericHID(port); + } + + /** + * Construct an instance of a device. + * + * @param port The port index on the Driver Station that the device is plugged into. + */ + public CommandGenericHID(int port) { + this(Scheduler.getDefault(), port); + } + + /** + * Get the underlying GenericHID object. + * + * @return the wrapped GenericHID object + */ + public GenericHID getHID() { + return m_hid; + } + + /** + * Constructs an event instance around this button's digital signal. + * + * @param button the button index + * @return an event instance representing the button's digital signal attached to the {@link + * Scheduler#getDefaultEventLoop() default scheduler button loop}. + * @see #button(int, EventLoop) + */ + public Trigger button(int button) { + return button(button, m_scheduler.getDefaultEventLoop()); + } + + /** + * Constructs an event instance around this button's digital signal. + * + * @param button the button index + * @param loop the event loop instance to attach the event to. + * @return an event instance representing the button's digital signal attached to the given loop. + */ + public Trigger button(int button, EventLoop loop) { + var cache = m_buttonCache.computeIfAbsent(loop, k -> new HashMap<>()); + return cache.computeIfAbsent( + button, k -> new Trigger(m_scheduler, loop, () -> m_hid.getRawButton(k))); + } + + /** + * Constructs a Trigger instance based around this angle of the default (index 0) POV on the HID, + * attached to {@link Scheduler#getDefaultEventLoop() the default command scheduler button loop}. + * + * @param angle POV angle + * @return a Trigger instance based around this angle of a POV on the HID. + */ + public Trigger pov(POVDirection angle) { + return pov(0, angle, m_scheduler.getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance based around this angle of a POV on the HID. + * + * @param pov index of the POV to read (starting at 0). Defaults to 0. + * @param angle POV angle + * @param loop the event loop instance to attach the event to. Defaults to {@link + * Scheduler#getDefaultEventLoop() the default command scheduler button loop}. + * @return a Trigger instance based around this angle of a POV on the HID. + */ + public Trigger pov(int pov, POVDirection angle, EventLoop loop) { + var cache = m_povCache.computeIfAbsent(loop, k -> new HashMap<>()); + // angle.value is a 4 bit bitfield + return cache.computeIfAbsent( + pov * 16 + angle.value, + k -> new Trigger(m_scheduler, loop, () -> m_hid.getPOV(pov) == angle)); + } + + /** + * Constructs a Trigger instance based around the 0 degree angle (up) of the default (index 0) POV + * on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default command scheduler + * button loop}. + * + * @return a Trigger instance based around the 0 degree angle of a POV on the HID. + */ + public Trigger povUp() { + return pov(POVDirection.Up); + } + + /** + * Constructs a Trigger instance based around the 45 degree angle (right up) of the default (index + * 0) POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default command + * scheduler button loop}. + * + * @return a Trigger instance based around the 45 degree angle of a POV on the HID. + */ + public Trigger povUpRight() { + return pov(POVDirection.UpRight); + } + + /** + * Constructs a Trigger instance based around the 90 degree angle (right) of the default (index 0) + * POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default command + * scheduler button loop}. + * + * @return a Trigger instance based around the 90 degree angle of a POV on the HID. + */ + public Trigger povRight() { + return pov(POVDirection.Right); + } + + /** + * Constructs a Trigger instance based around the 135 degree angle (right down) of the default + * (index 0) POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default + * command scheduler button loop}. + * + * @return a Trigger instance based around the 135 degree angle of a POV on the HID. + */ + public Trigger povDownRight() { + return pov(POVDirection.DownRight); + } + + /** + * Constructs a Trigger instance based around the 180 degree angle (down) of the default (index 0) + * POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default command + * scheduler button loop}. + * + * @return a Trigger instance based around the 180 degree angle of a POV on the HID. + */ + public Trigger povDown() { + return pov(POVDirection.Down); + } + + /** + * Constructs a Trigger instance based around the 225 degree angle (down left) of the default + * (index 0) POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default + * command scheduler button loop}. + * + * @return a Trigger instance based around the 225 degree angle of a POV on the HID. + */ + public Trigger povDownLeft() { + return pov(POVDirection.DownLeft); + } + + /** + * Constructs a Trigger instance based around the 270 degree angle (left) of the default (index 0) + * POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default command + * scheduler button loop}. + * + * @return a Trigger instance based around the 270 degree angle of a POV on the HID. + */ + public Trigger povLeft() { + return pov(POVDirection.Left); + } + + /** + * Constructs a Trigger instance based around the 315 degree angle (left up) of the default (index + * 0) POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default command + * scheduler button loop}. + * + * @return a Trigger instance based around the 315 degree angle of a POV on the HID. + */ + public Trigger povUpLeft() { + return pov(POVDirection.UpLeft); + } + + /** + * Constructs a Trigger instance based around the center (not pressed) position of the default + * (index 0) POV on the HID, attached to {@link Scheduler#getDefaultEventLoop() the default + * command scheduler button loop}. + * + * @return a Trigger instance based around the center position of a POV on the HID. + */ + public Trigger povCenter() { + return pov(POVDirection.Center); + } + + /** + * Constructs a Trigger instance that is true when the axis value is less than {@code threshold}, + * attached to {@link Scheduler#getDefaultEventLoop() the default command scheduler button loop}. + * + * @param axis The axis to read, starting at 0 + * @param threshold The value below which this trigger should return true. + * @return a Trigger instance that is true when the axis value is less than the provided + * threshold. + */ + public Trigger axisLessThan(int axis, double threshold) { + return axisLessThan(axis, threshold, m_scheduler.getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance that is true when the axis value is less than {@code threshold}, + * attached to the given loop. + * + * @param axis The axis to read, starting at 0 + * @param threshold The value below which this trigger should return true. + * @param loop the event loop instance to attach the trigger to + * @return a Trigger instance that is true when the axis value is less than the provided + * threshold. + */ + public Trigger axisLessThan(int axis, double threshold, EventLoop loop) { + var cache = m_axisLessThanCache.computeIfAbsent(loop, k -> new HashMap<>()); + return cache.computeIfAbsent( + Pair.of(axis, threshold), + k -> new Trigger(m_scheduler, loop, () -> getRawAxis(axis) < threshold)); + } + + /** + * Constructs a Trigger instance that is true when the axis value is less than {@code threshold}, + * attached to {@link Scheduler#getDefaultEventLoop() the default command scheduler button loop}. + * + * @param axis The axis to read, starting at 0 + * @param threshold The value above which this trigger should return true. + * @return a Trigger instance that is true when the axis value is greater than the provided + * threshold. + */ + public Trigger axisGreaterThan(int axis, double threshold) { + return axisGreaterThan(axis, threshold, m_scheduler.getDefaultEventLoop()); + } + + /** + * Constructs a Trigger instance that is true when the axis value is greater than {@code + * threshold}, attached to the given loop. + * + * @param axis The axis to read, starting at 0 + * @param threshold The value above which this trigger should return true. + * @param loop the event loop instance to attach the trigger to. + * @return a Trigger instance that is true when the axis value is greater than the provided + * threshold. + */ + public Trigger axisGreaterThan(int axis, double threshold, EventLoop loop) { + var cache = m_axisGreaterThanCache.computeIfAbsent(loop, k -> new HashMap<>()); + return cache.computeIfAbsent( + Pair.of(axis, threshold), + k -> new Trigger(m_scheduler, loop, () -> getRawAxis(axis) > threshold)); + } + + /** + * Constructs a Trigger instance that is true when the axis magnitude value is greater than {@code + * threshold}, attached to the given loop. + * + * @param axis The axis to read, starting at 0 + * @param threshold The value above which this trigger should return true. + * @param loop the event loop instance to attach the trigger to. + * @return a Trigger instance that is true when the axis magnitude value is greater than the + * provided threshold. + */ + public Trigger axisMagnitudeGreaterThan(int axis, double threshold, EventLoop loop) { + var cache = m_axisMagnitudeGreaterThanCache.computeIfAbsent(loop, k -> new HashMap<>()); + return cache.computeIfAbsent( + Pair.of(axis, threshold), + k -> new Trigger(m_scheduler, loop, () -> Math.abs(getRawAxis(axis)) > threshold)); + } + + /** + * Constructs a Trigger instance that is true when the axis magnitude value is greater than {@code + * threshold}, attached to {@link Scheduler#getDefaultEventLoop() the default command scheduler + * button loop}. + * + * @param axis The axis to read, starting at 0 + * @param threshold The value above which this trigger should return true. + * @return a Trigger instance that is true when the deadbanded axis value is active (non-zero). + */ + public Trigger axisMagnitudeGreaterThan(int axis, double threshold) { + return axisMagnitudeGreaterThan(axis, threshold, m_scheduler.getDefaultEventLoop()); + } + + /** + * Get the value of the axis. + * + * @param axis The axis to read, starting at 0. + * @return The value of the axis. + */ + public double getRawAxis(int axis) { + return m_hid.getRawAxis(axis); + } + + /** + * Set the rumble output for the HID. The DS currently supports 2 rumble values, left rumble and + * right rumble. + * + * @param type Which rumble value to set + * @param value The normalized value (0 to 1) to set the rumble to + */ + public void setRumble(GenericHID.RumbleType type, double value) { + m_hid.setRumble(type, value); + } + + /** + * Get if the HID is connected. + * + * @return true if the HID is connected + */ + public boolean isConnected() { + return m_hid.isConnected(); + } + + /** + * Gets the scheduler that should execute the triggered commands. This scheduler is set in the + * constructor, defaulting to {@link Scheduler#getDefault()} if one was not provided. + * + * @return the scheduler that should execute the triggered commands + */ + protected final Scheduler getScheduler() { + return m_scheduler; + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/button/CommandJoystick.java b/commandsv3/src/main/java/org/wpilib/commands3/button/CommandJoystick.java new file mode 100644 index 0000000000..cc7bf9a335 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/button/CommandJoystick.java @@ -0,0 +1,275 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.button; + +import edu.wpi.first.wpilibj.Joystick; +import edu.wpi.first.wpilibj.event.EventLoop; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.Trigger; + +/** + * A version of {@link Joystick} with {@link Trigger} factories for command-based. + * + * @see Joystick + */ +public class CommandJoystick extends CommandGenericHID { + private final Joystick m_hid; + + /** + * Construct an instance of a controller. + * + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandJoystick(int port) { + super(port); + m_hid = new Joystick(port); + } + + /** + * Construct an instance of a controller. + * + * @param scheduler The scheduler that should execute the triggered commands. + * @param port The port index on the Driver Station that the controller is plugged into. + */ + public CommandJoystick(Scheduler scheduler, int port) { + super(scheduler, port); + m_hid = new Joystick(port); + } + + /** + * Get the underlying GenericHID object. + * + * @return the wrapped GenericHID object + */ + @Override + public Joystick getHID() { + return m_hid; + } + + /** + * Constructs an event instance around the trigger button's digital signal. + * + * @return an event instance representing the trigger button's digital signal attached to the + * {@link Scheduler#getDefaultEventLoop() default scheduler button loop}. + * @see #trigger(EventLoop) + */ + public Trigger trigger() { + return trigger(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs an event instance around the trigger button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return an event instance representing the trigger button's digital signal attached to the + * given loop. + */ + public Trigger trigger(EventLoop loop) { + return button(Joystick.ButtonType.kTrigger.value, loop); + } + + /** + * Constructs an event instance around the top button's digital signal. + * + * @return an event instance representing the top button's digital signal attached to the {@link + * Scheduler#getDefaultEventLoop() default scheduler button loop}. + * @see #top(EventLoop) + */ + public Trigger top() { + return top(getScheduler().getDefaultEventLoop()); + } + + /** + * Constructs an event instance around the top button's digital signal. + * + * @param loop the event loop instance to attach the event to. + * @return an event instance representing the top button's digital signal attached to the given + * loop. + */ + public Trigger top(EventLoop loop) { + return button(Joystick.ButtonType.kTop.value, loop); + } + + /** + * Set the channel associated with the X axis. + * + * @param channel The channel to set the axis to. + */ + public void setXChannel(int channel) { + m_hid.setXChannel(channel); + } + + /** + * Set the channel associated with the Y axis. + * + * @param channel The channel to set the axis to. + */ + public void setYChannel(int channel) { + m_hid.setYChannel(channel); + } + + /** + * Set the channel associated with the Z axis. + * + * @param channel The channel to set the axis to. + */ + public void setZChannel(int channel) { + m_hid.setZChannel(channel); + } + + /** + * Set the channel associated with the throttle axis. + * + * @param channel The channel to set the axis to. + */ + public void setThrottleChannel(int channel) { + m_hid.setThrottleChannel(channel); + } + + /** + * Set the channel associated with the twist axis. + * + * @param channel The channel to set the axis to. + */ + public void setTwistChannel(int channel) { + m_hid.setTwistChannel(channel); + } + + /** + * Get the channel currently associated with the X axis. + * + * @return The channel for the axis. + */ + public int getXChannel() { + return m_hid.getXChannel(); + } + + /** + * Get the channel currently associated with the Y axis. + * + * @return The channel for the axis. + */ + public int getYChannel() { + return m_hid.getYChannel(); + } + + /** + * Get the channel currently associated with the Z axis. + * + * @return The channel for the axis. + */ + public int getZChannel() { + return m_hid.getZChannel(); + } + + /** + * Get the channel currently associated with the twist axis. + * + * @return The channel for the axis. + */ + public int getTwistChannel() { + return m_hid.getTwistChannel(); + } + + /** + * Get the channel currently associated with the throttle axis. + * + * @return The channel for the axis. + */ + public int getThrottleChannel() { + return m_hid.getThrottleChannel(); + } + + /** + * Get the x position of the HID. + * + *

This depends on the mapping of the joystick connected to the current port. On most + * joysticks, positive is to the right. + * + * @return the x position + */ + public double getX() { + return m_hid.getX(); + } + + /** + * Get the y position of the HID. + * + *

This depends on the mapping of the joystick connected to the current port. On most + * joysticks, positive is to the back. + * + * @return the y position + */ + public double getY() { + return m_hid.getY(); + } + + /** + * Get the z position of the HID. + * + * @return the z position + */ + public double getZ() { + return m_hid.getZ(); + } + + /** + * Get the twist value of the current joystick. This depends on the mapping of the joystick + * connected to the current port. + * + * @return The Twist value of the joystick. + */ + public double getTwist() { + return m_hid.getTwist(); + } + + /** + * Get the throttle value of the current joystick. This depends on the mapping of the joystick + * connected to the current port. + * + * @return The Throttle value of the joystick. + */ + public double getThrottle() { + return m_hid.getThrottle(); + } + + /** + * Get the magnitude of the vector formed by the joystick's current position relative to its + * origin. + * + * @return The magnitude of the direction vector + */ + public double getMagnitude() { + return m_hid.getMagnitude(); + } + + /** + * Get the direction of the vector formed by the joystick and its origin in radians. 0 is forward + * and clockwise is positive. (Straight right is π/2.) + * + * @return The direction of the vector in radians + */ + public double getDirectionRadians() { + // https://docs.wpilib.org/en/stable/docs/software/basic-programming/coordinate-system.html#joystick-and-controller-coordinate-system + // A positive rotation around the X axis moves the joystick right, and a + // positive rotation around the Y axis moves the joystick backward. When + // treating them as translations, 0 radians is measured from the right + // direction, and angle increases clockwise. + // + // It's rotated 90 degrees CCW (y is negated and the arguments are reversed) + // so that 0 radians is forward. + return m_hid.getDirectionRadians(); + } + + /** + * Get the direction of the vector formed by the joystick and its origin in degrees. 0 is forward + * and clockwise is positive. (Straight right is 90.) + * + * @return The direction of the vector in degrees + */ + public double getDirectionDegrees() { + return m_hid.getDirectionDegrees(); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/button/InternalButton.java b/commandsv3/src/main/java/org/wpilib/commands3/button/InternalButton.java new file mode 100644 index 0000000000..6bedec6d7f --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/button/InternalButton.java @@ -0,0 +1,61 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.button; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.wpilib.commands3.Trigger; + +/** + * This class is intended to be used within a program. The programmer can manually set its value. + * Also includes a setting for whether it should invert its value. + */ +public class InternalButton extends Trigger { + // need to be references, so they can be mutated after being captured in the constructor. + private final AtomicBoolean m_pressed; + private final AtomicBoolean m_inverted; + + /** Creates an InternalButton that is not inverted. */ + public InternalButton() { + this(false); + } + + /** + * Creates an InternalButton which is inverted depending on the input. + * + * @param inverted if false, then this button is pressed when set to true, otherwise it is pressed + * when set to false. + */ + public InternalButton(boolean inverted) { + this(new AtomicBoolean(), new AtomicBoolean(inverted)); + } + + /* + * Mock constructor so the AtomicBoolean objects can be constructed before the super + * constructor invocation. + */ + private InternalButton(AtomicBoolean state, AtomicBoolean inverted) { + super(() -> state.get() != inverted.get()); + m_pressed = state; + m_inverted = inverted; + } + + /** + * Sets whether to invert button state. + * + * @param inverted Whether button state should be inverted. + */ + public void setInverted(boolean inverted) { + m_inverted.set(inverted); + } + + /** + * Sets whether button is pressed. + * + * @param pressed Whether button is pressed. + */ + public void setPressed(boolean pressed) { + m_pressed.set(pressed); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/button/JoystickButton.java b/commandsv3/src/main/java/org/wpilib/commands3/button/JoystickButton.java new file mode 100644 index 0000000000..5c968a939f --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/button/JoystickButton.java @@ -0,0 +1,24 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.button; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import edu.wpi.first.wpilibj.GenericHID; +import org.wpilib.commands3.Trigger; + +/** A {@link Trigger} that gets its state from a {@link GenericHID}. */ +public class JoystickButton extends Trigger { + /** + * Creates a joystick button for triggering commands. + * + * @param joystick The GenericHID object that has the button (e.g. Joystick, KinectStick, etc) + * @param buttonNumber The button number (see {@link GenericHID#getRawButton(int) } + */ + public JoystickButton(GenericHID joystick, int buttonNumber) { + super(() -> joystick.getRawButton(buttonNumber)); + requireNonNullParam(joystick, "joystick", "JoystickButton"); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/button/NetworkButton.java b/commandsv3/src/main/java/org/wpilib/commands3/button/NetworkButton.java new file mode 100644 index 0000000000..330160339e --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/button/NetworkButton.java @@ -0,0 +1,66 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.button; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import edu.wpi.first.networktables.BooleanSubscriber; +import edu.wpi.first.networktables.BooleanTopic; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; +import org.wpilib.commands3.Trigger; + +/** A {@link Trigger} that uses a {@link NetworkTable} boolean field. */ +public class NetworkButton extends Trigger { + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param topic The boolean topic that contains the value. + */ + public NetworkButton(BooleanTopic topic) { + this(topic.subscribe(false)); + } + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param sub The boolean subscriber that provides the value. + */ + public NetworkButton(BooleanSubscriber sub) { + super(() -> sub.getTopic().getInstance().isConnected() && sub.get()); + requireNonNullParam(sub, "sub", "NetworkButton"); + } + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param table The table where the networktable value is located. + * @param field The field that is the value. + */ + public NetworkButton(NetworkTable table, String field) { + this(table.getBooleanTopic(field)); + } + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param table The table where the networktable value is located. + * @param field The field that is the value. + */ + public NetworkButton(String table, String field) { + this(NetworkTableInstance.getDefault(), table, field); + } + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param inst The NetworkTable instance to use + * @param table The table where the networktable value is located. + * @param field The field that is the value. + */ + public NetworkButton(NetworkTableInstance inst, String table, String field) { + this(inst.getTable(table), field); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/button/POVButton.java b/commandsv3/src/main/java/org/wpilib/commands3/button/POVButton.java new file mode 100644 index 0000000000..154ddbc3f9 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/button/POVButton.java @@ -0,0 +1,36 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.button; + +import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; + +import edu.wpi.first.wpilibj.DriverStation.POVDirection; +import edu.wpi.first.wpilibj.GenericHID; +import org.wpilib.commands3.Trigger; + +/** A {@link Trigger} that gets its state from a POV on a {@link GenericHID}. */ +public class POVButton extends Trigger { + /** + * Creates a POV button for triggering commands. + * + * @param joystick The GenericHID object that has the POV + * @param angle The desired angle + * @param povNumber The POV number (see {@link GenericHID#getPOV(int)}) + */ + public POVButton(GenericHID joystick, POVDirection angle, int povNumber) { + super(() -> joystick.getPOV(povNumber) == angle); + requireNonNullParam(joystick, "joystick", "POVButton"); + } + + /** + * Creates a POV button for triggering commands. By default, acts on POV 0 + * + * @param joystick The GenericHID object that has the POV + * @param angle The desired angle + */ + public POVButton(GenericHID joystick, POVDirection angle) { + this(joystick, angle, 0); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/button/RobotModeTriggers.java b/commandsv3/src/main/java/org/wpilib/commands3/button/RobotModeTriggers.java new file mode 100644 index 0000000000..6a84df1ff0 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/button/RobotModeTriggers.java @@ -0,0 +1,53 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.button; + +import edu.wpi.first.wpilibj.DriverStation; +import org.wpilib.commands3.Trigger; + +/** + * A class containing static {@link Trigger} factories for running callbacks when the robot mode + * changes. + */ +public final class RobotModeTriggers { + // Utility class + private RobotModeTriggers() {} + + /** + * Returns a trigger that is true when the robot is enabled in autonomous mode. + * + * @return A trigger that is true when the robot is enabled in autonomous mode. + */ + public static Trigger autonomous() { + return new Trigger(DriverStation::isAutonomousEnabled); + } + + /** + * Returns a trigger that is true when the robot is enabled in teleop mode. + * + * @return A trigger that is true when the robot is enabled in teleop mode. + */ + public static Trigger teleop() { + return new Trigger(DriverStation::isTeleopEnabled); + } + + /** + * Returns a trigger that is true when the robot is disabled. + * + * @return A trigger that is true when the robot is disabled. + */ + public static Trigger disabled() { + return new Trigger(DriverStation::isDisabled); + } + + /** + * Returns a trigger that is true when the robot is enabled in test mode. + * + * @return A trigger that is true when the robot is enabled in test mode. + */ + public static Trigger test() { + return new Trigger(DriverStation::isTestEnabled); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/proto/CommandProto.java b/commandsv3/src/main/java/org/wpilib/commands3/proto/CommandProto.java new file mode 100644 index 0000000000..f8fe21f771 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/proto/CommandProto.java @@ -0,0 +1,65 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.proto; + +import edu.wpi.first.util.protobuf.Protobuf; +import org.wpilib.commands3.Command; +import org.wpilib.commands3.Mechanism; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.proto.ProtobufCommands.ProtobufCommand; +import us.hebi.quickbuf.Descriptors; + +/** Protobuf serde for running commands. */ +public class CommandProto implements Protobuf { + private final Scheduler m_scheduler; + + public CommandProto(Scheduler scheduler) { + m_scheduler = scheduler; + } + + @Override + public Class getTypeClass() { + return Command.class; + } + + @Override + public Descriptors.Descriptor getDescriptor() { + return ProtobufCommand.getDescriptor(); + } + + @Override + public ProtobufCommand createMessage() { + return ProtobufCommand.newInstance(); + } + + @Override + public Command unpack(ProtobufCommand msg) { + // Not possible. The command behavior is what really matters, and it cannot be serialized + throw new UnsupportedOperationException("Deserialization not supported"); + } + + @Override + public void pack(ProtobufCommand msg, Command command) { + msg.clear(); + + msg.setId(m_scheduler.runId(command)); + Command parent = m_scheduler.getParentOf(command); + if (parent != null) { + msg.setParentId(m_scheduler.runId(parent)); + } + msg.setName(command.name()); + msg.setPriority(command.priority()); + + Protobuf.packArray( + msg.getMutableRequirements(), + command.requirements().toArray(new Mechanism[0]), + new MechanismProto()); + + if (m_scheduler.isRunning(command)) { + msg.setLastTimeMs(m_scheduler.lastCommandRuntimeMs(command)); + msg.setTotalTimeMs(m_scheduler.totalRuntimeMs(command)); + } + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/proto/MechanismProto.java b/commandsv3/src/main/java/org/wpilib/commands3/proto/MechanismProto.java new file mode 100644 index 0000000000..5e469d2d54 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/proto/MechanismProto.java @@ -0,0 +1,38 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.proto; + +import edu.wpi.first.util.protobuf.Protobuf; +import org.wpilib.commands3.Mechanism; +import org.wpilib.commands3.proto.ProtobufCommands.ProtobufMechanism; +import us.hebi.quickbuf.Descriptors; + +public class MechanismProto implements Protobuf { + @Override + public Class getTypeClass() { + return Mechanism.class; + } + + @Override + public Descriptors.Descriptor getDescriptor() { + return ProtobufMechanism.getDescriptor(); + } + + @Override + public ProtobufMechanism createMessage() { + return ProtobufMechanism.newInstance(); + } + + @Override + public Mechanism unpack(ProtobufMechanism msg) { + throw new UnsupportedOperationException("Deserialization not supported"); + } + + @Override + public void pack(ProtobufMechanism msg, Mechanism value) { + msg.clear(); + msg.setName(value.getName()); + } +} diff --git a/commandsv3/src/main/java/org/wpilib/commands3/proto/SchedulerProto.java b/commandsv3/src/main/java/org/wpilib/commands3/proto/SchedulerProto.java new file mode 100644 index 0000000000..bc5ad124c4 --- /dev/null +++ b/commandsv3/src/main/java/org/wpilib/commands3/proto/SchedulerProto.java @@ -0,0 +1,58 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3.proto; + +import edu.wpi.first.util.protobuf.Protobuf; +import org.wpilib.commands3.Command; +import org.wpilib.commands3.Scheduler; +import org.wpilib.commands3.proto.ProtobufCommands.ProtobufScheduler; +import us.hebi.quickbuf.Descriptors; + +/** + * Serializes a {@link Scheduler} to a protobuf message. Deserialization is not supported. A + * serialized message will include information about commands that are currently running or + * scheduled (but not yet started), as well as how long the most recent call to {@link + * Scheduler#run()} took to execute. + */ +public class SchedulerProto implements Protobuf { + @Override + public Class getTypeClass() { + return Scheduler.class; + } + + @Override + public Descriptors.Descriptor getDescriptor() { + return ProtobufScheduler.getDescriptor(); + } + + @Override + public ProtobufScheduler createMessage() { + return ProtobufScheduler.newInstance(); + } + + @Override + public Scheduler unpack(ProtobufScheduler msg) { + throw new UnsupportedOperationException("Deserialization not supported"); + } + + @Override + public void pack(ProtobufScheduler msg, Scheduler scheduler) { + msg.clear(); + + var commandProto = new CommandProto(scheduler); + + Protobuf.packArray( + msg.getMutableQueuedCommands(), + scheduler.getQueuedCommands().toArray(new Command[0]), + commandProto); + + Protobuf.packArray( + msg.getMutableRunningCommands(), + scheduler.getRunningCommands().toArray(new Command[0]), + commandProto); + + msg.setLastTimeMs(scheduler.lastRuntimeMs()); + } +} diff --git a/commandsv3/src/main/proto/protobuf_commands.proto b/commandsv3/src/main/proto/protobuf_commands.proto new file mode 100644 index 0000000000..beaba6b8c0 --- /dev/null +++ b/commandsv3/src/main/proto/protobuf_commands.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package wpi.proto; + +option java_package = "org.wpilib.commands3.proto"; + +/* +Or use the generate_files.py script: + +# macOS +allwpilib $ ./commandsv3/generate_files.py --protoc=protoc-quickbuf + +# Linux +allwpilib $ ./commandsv3/generate_files.py --quickbuf_plugin protoc-gen-quickbuf-1.3.3-linux-x86_64.exe + */ + +message ProtobufMechanism { + string name = 1; +} + +message ProtobufCommand { + // A unique ID for the command. + // Different invocations of the same command object have different IDs. + uint32 id = 1; + + // The ID of the parent command. + // Not included in the message for top-level commands. + optional uint32 parent_id = 2; + + // The name of the command. + string name = 3; + + // The priority level of the command. + int32 priority = 4; + + // The mechanisms required by the command. + repeated ProtobufMechanism requirements = 5; + + // How much time the command took to execute in its most recent run. + // Only included in a message for an actively running command. + optional double last_time_ms = 6; + + // How long the command has taken to run, in aggregate. + // Only included in a message for an actively running command. + optional double total_time_ms = 7; +} + +message ProtobufScheduler { + // Note: commands are generally queued by triggers, which occurs immediately before they are + // promoted and start running. Entries will only appear here when serializing a scheduler + // _after_ manually scheduling a command but _before_ calling scheduler.run() + repeated ProtobufCommand queued_commands = 1; + repeated ProtobufCommand running_commands = 2; + + // How much time the scheduler took in its last `run()` invocation. + double last_time_ms = 3; +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/CommandTestBase.java b/commandsv3/src/test/java/org/wpilib/commands3/CommandTestBase.java new file mode 100644 index 0000000000..0423924397 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/CommandTestBase.java @@ -0,0 +1,23 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import edu.wpi.first.wpilibj.RobotController; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; + +class CommandTestBase { + protected Scheduler m_scheduler; + protected List m_events; + + @BeforeEach + void initScheduler() { + RobotController.setTimeSource(() -> System.nanoTime() / 1000L); + m_scheduler = Scheduler.createIndependentScheduler(); + m_events = new ArrayList<>(); + m_scheduler.addEventListener(m_events::add); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/ConflictDetectorTest.java b/commandsv3/src/test/java/org/wpilib/commands3/ConflictDetectorTest.java new file mode 100644 index 0000000000..ae1e5ef862 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/ConflictDetectorTest.java @@ -0,0 +1,70 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.wpilib.commands3.ConflictDetector.findAllConflicts; +import static org.wpilib.commands3.ConflictDetector.throwIfConflicts; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.wpilib.commands3.ConflictDetector.Conflict; + +class ConflictDetectorTest extends CommandTestBase { + @Test + void emptyInputHasNoConflicts() { + var conflicts = findAllConflicts(List.of()); + assertEquals(0, conflicts.size()); + } + + @Test + void singleInputHasNoConflicts() { + var mech = new Mechanism("Mech", m_scheduler); + var command = Command.requiring(mech).executing(Coroutine::park).named("Command"); + var conflicts = findAllConflicts(List.of(command)); + assertEquals(0, conflicts.size()); + } + + @Test + void commandDoesNotConflictWithSelf() { + var mech = new Mechanism("Mech", m_scheduler); + var command = Command.requiring(mech).executing(Coroutine::park).named("Command"); + var conflicts = findAllConflicts(List.of(command, command)); + assertEquals(0, conflicts.size()); + } + + @Test + void detectManyConflicts() { + var mech1 = new Mechanism("Mech 1", m_scheduler); + var mech2 = new Mechanism("Mech 2", m_scheduler); + + var command1 = Command.requiring(mech1, mech2).executing(Coroutine::park).named("Command1"); + var command2 = Command.requiring(mech1).executing(Coroutine::park).named("Command2"); + var command3 = Command.requiring(mech2).executing(Coroutine::park).named("Command3"); + var command4 = Command.requiring(mech2, mech1).executing(Coroutine::park).named("Command4"); + var allCommands = List.of(command1, command2, command3, command4); + + var conflicts = findAllConflicts(allCommands); + assertEquals(5, conflicts.size(), "Five conflicting pairs should have been found"); + assertEquals(new Conflict(command1, command2, Set.of(mech1)), conflicts.get(0)); + assertEquals(new Conflict(command1, command3, Set.of(mech2)), conflicts.get(1)); + assertEquals(new Conflict(command1, command4, Set.of(mech1, mech2)), conflicts.get(2)); + assertEquals(new Conflict(command2, command4, Set.of(mech1)), conflicts.get(3)); + assertEquals(new Conflict(command3, command4, Set.of(mech2)), conflicts.get(4)); + + // error messaging + var error = assertThrows(IllegalArgumentException.class, () -> throwIfConflicts(allCommands)); + assertEquals( + "Commands running in parallel cannot share requirements: " + + "Command1 and Command2 both require Mech 1; " + + "Command1 and Command3 both require Mech 2; " + + "Command1 and Command4 both require Mech 1, Mech 2; " + + "Command2 and Command4 both require Mech 1; " + + "Command3 and Command4 both require Mech 2", + error.getMessage()); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/CoroutineTest.java b/commandsv3/src/test/java/org/wpilib/commands3/CoroutineTest.java new file mode 100644 index 0000000000..b6779f8a9a --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/CoroutineTest.java @@ -0,0 +1,174 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.junit.jupiter.api.Test; + +class CoroutineTest extends CommandTestBase { + @Test + void forkMany() { + var a = new NullCommand(); + var b = new NullCommand(); + var c = new NullCommand(); + + var all = + Command.noRequirements() + .executing( + co -> { + co.fork(a, b, c); + co.park(); + }) + .named("Fork Many"); + + m_scheduler.schedule(all); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(a)); + assertTrue(m_scheduler.isRunning(b)); + assertTrue(m_scheduler.isRunning(c)); + } + + @Test + void yieldInSynchronizedBlock() { + Object mutex = new Object(); + AtomicInteger i = new AtomicInteger(0); + + var yieldInSynchronized = + Command.noRequirements() + .executing( + co -> { + while (true) { + synchronized (mutex) { + i.incrementAndGet(); + co.yield(); + } + } + }) + .named("Yield In Synchronized Block"); + + m_scheduler.schedule(yieldInSynchronized); + + var error = assertThrows(IllegalStateException.class, m_scheduler::run); + assertEquals( + "Coroutine.yield() cannot be called inside a synchronized block or method. " + + "Consider using a Lock instead of synchronized, " + + "or rewrite your code to avoid locks and mutexes altogether.", + error.getMessage()); + } + + @Test + void yieldInLockBody() { + Lock lock = new ReentrantLock(); + AtomicInteger i = new AtomicInteger(0); + + var yieldInLock = + Command.noRequirements() + .executing( + co -> { + while (true) { + lock.lock(); + try { + i.incrementAndGet(); + co.yield(); + } finally { + lock.unlock(); + } + } + }) + .named("Increment In Lock Block"); + + m_scheduler.schedule(yieldInLock); + m_scheduler.run(); + assertEquals(1, i.get()); + } + + @Test + void coroutineEscapingCommand() { + AtomicReference escapeeCallback = new AtomicReference<>(); + + var badCommand = + Command.noRequirements() + .executing( + co -> { + escapeeCallback.set(co::yield); + }) + .named("Bad Command"); + + m_scheduler.schedule(badCommand); + m_scheduler.run(); + + var error = assertThrows(IllegalStateException.class, escapeeCallback.get()::run); + assertEquals("Coroutines can only be used by the command bound to them", error.getMessage()); + } + + @Test + void usingParentCoroutineInChildThrows() { + var parent = + Command.noRequirements() + .executing( + parentCoroutine -> { + parentCoroutine.await( + Command.noRequirements() + .executing( + childCoroutine -> { + parentCoroutine.yield(); + }) + .named("Child")); + }) + .named("Parent"); + + m_scheduler.schedule(parent); + var error = assertThrows(IllegalStateException.class, m_scheduler::run); + assertEquals("Coroutines can only be used by the command bound to them", error.getMessage()); + } + + @Test + void awaitAnyCleansUp() { + AtomicBoolean firstRan = new AtomicBoolean(false); + AtomicBoolean secondRan = new AtomicBoolean(false); + AtomicBoolean ranAfterAwait = new AtomicBoolean(false); + + var firstInner = Command.noRequirements().executing(c2 -> firstRan.set(true)).named("First"); + var secondInner = + Command.noRequirements() + .executing( + c2 -> { + secondRan.set(true); + c2.park(); + }) + .named("Second"); + + var outer = + Command.noRequirements() + .executing( + co -> { + co.awaitAny(firstInner, secondInner); + + ranAfterAwait.set(true); + co.park(); // prevent exiting + }) + .named("Command"); + + m_scheduler.schedule(outer); + m_scheduler.run(); + + // Everything should have run... + assertTrue(firstRan.get()); + assertTrue(secondRan.get()); + assertTrue(ranAfterAwait.get()); + + // But only the outer command should still be running; secondInner should have been canceled + assertEquals(Set.of(outer), m_scheduler.getRunningCommands()); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/NullCommand.java b/commandsv3/src/test/java/org/wpilib/commands3/NullCommand.java new file mode 100644 index 0000000000..e66060c7d4 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/NullCommand.java @@ -0,0 +1,24 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import java.util.Set; + +class NullCommand implements Command { + @Override + public void run(Coroutine coroutine) { + coroutine.park(); + } + + @Override + public String name() { + return "Null Command"; + } + + @Override + public Set requirements() { + return Set.of(); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/ParallelGroupTest.java b/commandsv3/src/test/java/org/wpilib/commands3/ParallelGroupTest.java new file mode 100644 index 0000000000..f89bd647f0 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/ParallelGroupTest.java @@ -0,0 +1,233 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class ParallelGroupTest extends CommandTestBase { + @Test + void parallelAll() { + var r1 = new Mechanism("R1", m_scheduler); + var r2 = new Mechanism("R2", m_scheduler); + + var c1Count = new AtomicInteger(0); + var c2Count = new AtomicInteger(0); + + var c1 = + r1.run( + coroutine -> { + for (int i = 0; i < 5; i++) { + coroutine.yield(); + c1Count.incrementAndGet(); + } + }) + .named("C1"); + var c2 = + r2.run( + coroutine -> { + for (int i = 0; i < 10; i++) { + coroutine.yield(); + c2Count.incrementAndGet(); + } + }) + .named("C2"); + + var parallel = new ParallelGroup("Parallel", List.of(c1, c2), List.of()); + m_scheduler.schedule(parallel); + + // First call to run() should schedule and start the commands + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(parallel)); + assertTrue(m_scheduler.isRunning(c1)); + assertTrue(m_scheduler.isRunning(c2)); + + // Next call to run() should start them + for (int i = 1; i < 5; i++) { + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(c1)); + assertTrue(m_scheduler.isRunning(c2)); + assertEquals(i, c1Count.get()); + assertEquals(i, c2Count.get()); + } + // c1 should finish after 5 iterations; c2 should continue for another 5 + for (int i = 5; i < 10; i++) { + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(c1)); + assertTrue(m_scheduler.isRunning(c2)); + assertEquals(5, c1Count.get()); + assertEquals(i, c2Count.get()); + } + + // one final run() should unschedule the c2 command and end the group + assertTrue(m_scheduler.isRunning(parallel)); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(c1)); + assertFalse(m_scheduler.isRunning(c2)); + assertFalse(m_scheduler.isRunning(parallel)); + + // and final counts should be 5 and 10 + assertEquals(5, c1Count.get()); + assertEquals(10, c2Count.get()); + } + + @Test + void race() { + var r1 = new Mechanism("R1", m_scheduler); + var r2 = new Mechanism("R2", m_scheduler); + + var c1Count = new AtomicInteger(0); + var c2Count = new AtomicInteger(0); + + var c1 = + r1.run( + coroutine -> { + for (int i = 0; i < 5; i++) { + coroutine.yield(); + c1Count.incrementAndGet(); + } + }) + .named("C1"); + var c2 = + r2.run( + coroutine -> { + for (int i = 0; i < 10; i++) { + coroutine.yield(); + c2Count.incrementAndGet(); + } + }) + .named("C2"); + + var race = new ParallelGroup("Race", List.of(), List.of(c1, c2)); + m_scheduler.schedule(race); + + // First call to run() should schedule the commands + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(race)); + assertTrue(m_scheduler.isRunning(c1)); + assertTrue(m_scheduler.isRunning(c2)); + + for (int i = 1; i < 5; i++) { + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(c1)); + assertTrue(m_scheduler.isRunning(c2)); + assertEquals(i, c1Count.get()); + assertEquals(i, c2Count.get()); + } + m_scheduler.run(); // complete c1 + assertFalse(m_scheduler.isRunning(race)); + assertFalse(m_scheduler.isRunning(c1)); + assertFalse(m_scheduler.isRunning(c2)); + + // and final counts should be 5 and 5 + assertEquals(5, c1Count.get()); + assertEquals(5, c2Count.get()); + } + + @Test + void nested() { + var mechanism = new Mechanism("mechanism", m_scheduler); + + var count = new AtomicInteger(0); + + var command = + mechanism + .run( + coroutine -> { + for (int i = 0; i < 5; i++) { + coroutine.yield(); + count.incrementAndGet(); + } + }) + .named("Command"); + + var inner = new ParallelGroup("Inner", Set.of(command), Set.of()); + var outer = new ParallelGroup("Outer", Set.of(), Set.of(inner)); + + // Scheduling: Outer group should be on deck + m_scheduler.schedule(outer); + assertTrue(m_scheduler.isScheduled(outer)); + assertFalse(m_scheduler.isScheduledOrRunning(inner)); + assertFalse(m_scheduler.isScheduledOrRunning(command)); + + // First run: Inner group and command should both be scheduled and running + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(outer), "Outer group should be running"); + assertTrue(m_scheduler.isRunning(inner), "Inner group should be running"); + assertTrue(m_scheduler.isRunning(command), "Command should be running"); + assertEquals(0, count.get()); + + // Runs 2 through 5: Outer and inner should both be running while the command runs + for (int i = 1; i < 5; i++) { + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(outer), "Outer group should be running"); + assertTrue(m_scheduler.isRunning(inner), "Inner group should be running"); + assertTrue(m_scheduler.isRunning(command), "Command should be running (" + i + ")"); + assertEquals(i, count.get()); + } + + // Run 6: Command should have completed naturally + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(outer), "Outer group should be running"); + assertFalse(m_scheduler.isRunning(inner), "Inner group should be running"); + assertFalse(m_scheduler.isRunning(command), "Command should have completed"); + } + + @Test + void automaticNameRace() { + var a = Command.noRequirements().executing(coroutine -> {}).named("A"); + var b = Command.noRequirements().executing(coroutine -> {}).named("B"); + var c = Command.noRequirements().executing(coroutine -> {}).named("C"); + + var group = new ParallelGroupBuilder().optional(a, b, c).withAutomaticName(); + assertEquals("(A | B | C)", group.name()); + } + + @Test + void automaticNameAll() { + var a = Command.noRequirements().executing(coroutine -> {}).named("A"); + var b = Command.noRequirements().executing(coroutine -> {}).named("B"); + var c = Command.noRequirements().executing(coroutine -> {}).named("C"); + + var group = new ParallelGroupBuilder().requiring(a, b, c).withAutomaticName(); + assertEquals("(A & B & C)", group.name()); + } + + @Test + void automaticNameDeadline() { + var a = Command.noRequirements().executing(coroutine -> {}).named("A"); + var b = Command.noRequirements().executing(coroutine -> {}).named("B"); + var c = Command.noRequirements().executing(coroutine -> {}).named("C"); + + var group = new ParallelGroupBuilder().requiring(a).optional(b, c).withAutomaticName(); + assertEquals("[(A) * (B | C)]", group.name()); + } + + @Test + void inheritsRequirements() { + var mech1 = new Mechanism("Mech 1", m_scheduler); + var mech2 = new Mechanism("Mech 2", m_scheduler); + var command1 = mech1.run(Coroutine::park).named("Command 1"); + var command2 = mech2.run(Coroutine::park).named("Command 2"); + var group = new ParallelGroup("Group", Set.of(command1, command2), Set.of()); + assertEquals(Set.of(mech1, mech2), group.requirements(), "Requirements were not inherited"); + } + + @Test + void inheritsPriority() { + var mech1 = new Mechanism("Mech 1", m_scheduler); + var mech2 = new Mechanism("Mech 2", m_scheduler); + var command1 = mech1.run(Coroutine::park).withPriority(100).named("Command 1"); + var command2 = mech2.run(Coroutine::park).withPriority(200).named("Command 2"); + var group = new ParallelGroup("Group", Set.of(command1, command2), Set.of()); + assertEquals(200, group.priority(), "Priority was not inherited"); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/PriorityCommand.java b/commandsv3/src/test/java/org/wpilib/commands3/PriorityCommand.java new file mode 100644 index 0000000000..6599b3f861 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/PriorityCommand.java @@ -0,0 +1,29 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import java.util.Set; + +record PriorityCommand(int priority, Mechanism... subsystems) implements Command { + @Override + public void run(Coroutine coroutine) { + coroutine.park(); + } + + @Override + public Set requirements() { + return Set.of(subsystems); + } + + @Override + public String name() { + return toString(); + } + + @Override + public String toString() { + return "PriorityCommand[priority=" + priority + ", subsystems=" + Set.of(subsystems) + "]"; + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerCancellationTests.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerCancellationTests.java new file mode 100644 index 0000000000..90d2dfffef --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerCancellationTests.java @@ -0,0 +1,369 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +class SchedulerCancellationTests extends CommandTestBase { + @Test + void cancelOnInterruptDoesNotResume() { + var count = new AtomicInteger(0); + + var mechanism = new Mechanism("mechanism", m_scheduler); + + var interrupter = + Command.requiring(mechanism) + .executing(coroutine -> {}) + .withPriority(2) + .named("Interrupter"); + + var canceledCommand = + Command.requiring(mechanism) + .executing( + coroutine -> { + count.set(1); + coroutine.yield(); + count.set(2); + }) + .withPriority(1) + .named("Cancel By Default"); + + m_scheduler.schedule(canceledCommand); + m_scheduler.run(); + + m_scheduler.schedule(interrupter); + m_scheduler.run(); + assertEquals(1, count.get()); // the second "set" call should not have run + } + + @Test + void defaultCommandResumesAfterInterruption() { + var count = new AtomicInteger(0); + + var mechanism = new Mechanism("mechanism", m_scheduler); + var defaultCmd = + mechanism + .run( + coroutine -> { + while (true) { + count.incrementAndGet(); + coroutine.yield(); + } + }) + .withPriority(-1) + .named("Default Command"); + + final var newerCmd = mechanism.run(coroutine -> {}).named("Newer Command"); + mechanism.setDefaultCommand(defaultCmd); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(defaultCmd), "Default command should be running"); + + m_scheduler.schedule(newerCmd); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(defaultCmd), "Default command should have been interrupted"); + assertEquals(1, count.get(), "Default command should have run once"); + + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(defaultCmd), "Default command should have resumed"); + assertEquals(2, count.get()); + } + + @Test + void cancelsEvictsOnDeck() { + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + m_scheduler.schedule(command); + m_scheduler.cancel(command); + assertFalse(m_scheduler.isScheduledOrRunning(command)); + } + + @Test + void commandCancelingSelf() { + var ranAfterCancel = new AtomicBoolean(false); + var commandRef = new AtomicReference(null); + var command = + Command.noRequirements() + .executing( + co -> { + co.scheduler().cancel(commandRef.get()); + ranAfterCancel.set(true); + }) + .named("Command"); + commandRef.set(command); + m_scheduler.schedule(command); + + var error = assertThrows(IllegalArgumentException.class, () -> m_scheduler.run()); + assertEquals("Command `Command` is mounted and cannot be canceled", error.getMessage()); + assertFalse(ranAfterCancel.get(), "Command should have stopped after encountering an error"); + assertFalse( + m_scheduler.isScheduledOrRunning(command), + "Command should have been removed from the scheduler"); + } + + @Test + void cancelAllEvictsOnDeck() { + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + m_scheduler.schedule(command); + m_scheduler.cancelAll(); + assertFalse(m_scheduler.isScheduledOrRunning(command)); + } + + @Test + void cancelAllCancelsAll() { + var commands = new ArrayList(10); + for (int i = 1; i <= 10; i++) { + commands.add(Command.noRequirements().executing(Coroutine::yield).named("Command " + i)); + } + commands.forEach(m_scheduler::schedule); + m_scheduler.run(); + m_scheduler.cancelAll(); + for (Command command : commands) { + if (m_scheduler.isRunning(command)) { + fail(command.name() + " was not canceled by cancelAll()"); + } + } + } + + @Test + void cancelAllCallsOnCancelHookForRunningCommands() { + AtomicBoolean ranHook = new AtomicBoolean(false); + var command = + Command.noRequirements() + .executing(Coroutine::park) + .whenCanceled(() -> ranHook.set(true)) + .named("Command"); + m_scheduler.schedule(command); + m_scheduler.run(); + m_scheduler.cancelAll(); + assertTrue(ranHook.get(), "onCancel hook was not called"); + } + + @Test + void cancelAllDoesNotCallOnCancelHookForQueuedCommands() { + AtomicBoolean ranHook = new AtomicBoolean(false); + var command = + Command.noRequirements() + .executing(Coroutine::park) + .whenCanceled(() -> ranHook.set(true)) + .named("Command"); + m_scheduler.schedule(command); + // no call to run before cancelAll() + m_scheduler.cancelAll(); + assertFalse(ranHook.get(), "onCancel hook was not called"); + } + + @Test + void cancelAllStartsDefaults() { + var mechanisms = new ArrayList(10); + for (int i = 1; i <= 10; i++) { + mechanisms.add(new Mechanism("System " + i, m_scheduler)); + } + + var command = Command.requiring(mechanisms).executing(Coroutine::yield).named("Big Command"); + + // Scheduling the command should evict the on-deck default commands + m_scheduler.schedule(command); + + // Then running should get it into the set of running commands + m_scheduler.run(); + + // Canceling should clear out the set of running commands + m_scheduler.cancelAll(); + + // Then ticking the scheduler once to fully remove the command and schedule the defaults + m_scheduler.run(); + + assertFalse(m_scheduler.isRunning(command), "Command was not canceled by cancelAll()"); + + for (var mechanism : mechanisms) { + var runningCommands = m_scheduler.getRunningCommandsFor(mechanism); + assertEquals( + 1, + runningCommands.size(), + "mechanism " + mechanism + " should have exactly one running command"); + assertEquals( + mechanism.getDefaultCommand(), + runningCommands.getFirst(), + "mechanism " + mechanism + " is not running the default command"); + } + } + + @Test + void cancelDeeplyNestedCompositions() { + Command root = + Command.noRequirements() + .executing( + co -> { + co.await( + Command.noRequirements() + .executing( + co2 -> { + co2.await( + Command.noRequirements() + .executing( + co3 -> { + co3.await( + Command.noRequirements() + .executing(Coroutine::park) + .named("Park")); + }) + .named("C3")); + }) + .named("C2")); + }) + .named("Root"); + + m_scheduler.schedule(root); + + m_scheduler.run(); + assertEquals(4, m_scheduler.getRunningCommands().size()); + + m_scheduler.cancel(root); + assertEquals(0, m_scheduler.getRunningCommands().size()); + } + + @Test + void compositionsDoNotSelfCancel() { + var mech = new Mechanism("The mechanism", m_scheduler); + var group = + mech.run( + co -> { + co.await( + mech.run( + co2 -> { + co2.await( + mech.run( + co3 -> { + co3.await(mech.run(Coroutine::park).named("Park")); + }) + .named("C3")); + }) + .named("C2")); + }) + .named("Group"); + + m_scheduler.schedule(group); + m_scheduler.run(); + assertEquals(4, m_scheduler.getRunningCommands().size()); + assertTrue(m_scheduler.isRunning(group)); + } + + @Test + void compositionsDoNotCancelParent() { + var mech = new Mechanism("The mechanism", m_scheduler); + var group = + mech.run( + co -> { + co.fork(mech.run(Coroutine::park).named("First Child")); + co.fork(mech.run(Coroutine::park).named("Second Child")); + co.park(); + }) + .named("Group"); + + m_scheduler.schedule(group); + m_scheduler.run(); + + // second child interrupts first child + assertEquals( + List.of("Group", "Second Child"), + m_scheduler.getRunningCommands().stream().map(Command::name).toList()); + } + + @Test + void doesNotRunOnCancelWhenInterruptingOnDeck() { + var ran = new AtomicBoolean(false); + + var mechanism = new Mechanism("The mechanism", m_scheduler); + var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd"); + var interrupter = mechanism.run(Coroutine::yield).named("Interrupter"); + m_scheduler.schedule(cmd); + m_scheduler.schedule(interrupter); + m_scheduler.run(); + + assertFalse(ran.get(), "onCancel ran when it shouldn't have!"); + } + + @Test + void doesNotRunOnCancelWhenCancelingOnDeck() { + var ran = new AtomicBoolean(false); + + var mechanism = new Mechanism("The mechanism", m_scheduler); + var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd"); + m_scheduler.schedule(cmd); + // canceling before calling .run() + m_scheduler.cancel(cmd); + m_scheduler.run(); + + assertFalse(ran.get(), "onCancel ran when it shouldn't have!"); + } + + @Test + void runsOnCancelWhenInterruptingCommand() { + var ran = new AtomicBoolean(false); + + var mechanism = new Mechanism("The mechanism", m_scheduler); + var cmd = mechanism.run(Coroutine::park).whenCanceled(() -> ran.set(true)).named("cmd"); + var interrupter = mechanism.run(Coroutine::park).named("Interrupter"); + m_scheduler.schedule(cmd); + m_scheduler.run(); + m_scheduler.schedule(interrupter); + m_scheduler.run(); + + assertTrue(ran.get(), "onCancel should have run!"); + } + + @Test + void doesNotRunOnCancelWhenCompleting() { + var ran = new AtomicBoolean(false); + + var mechanism = new Mechanism("The mechanism", m_scheduler); + var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd"); + m_scheduler.schedule(cmd); + m_scheduler.run(); + m_scheduler.run(); + + assertFalse(m_scheduler.isScheduledOrRunning(cmd)); + assertFalse(ran.get(), "onCancel ran when it shouldn't have!"); + } + + @Test + void runsOnCancelWhenCanceling() { + var ran = new AtomicBoolean(false); + + var mechanism = new Mechanism("The mechanism", m_scheduler); + var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd"); + m_scheduler.schedule(cmd); + m_scheduler.run(); + m_scheduler.cancel(cmd); + + assertTrue(ran.get(), "onCancel should have run!"); + } + + @Test + void runsOnCancelWhenCancelingParent() { + var ran = new AtomicBoolean(false); + + var mechanism = new Mechanism("The mechanism", m_scheduler); + var cmd = mechanism.run(Coroutine::yield).whenCanceled(() -> ran.set(true)).named("cmd"); + + var group = new SequentialGroup("Seq", Collections.singletonList(cmd)); + m_scheduler.schedule(group); + m_scheduler.run(); + m_scheduler.cancel(group); + + assertTrue(ran.get(), "onCancel should have run!"); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerConflictTests.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerConflictTests.java new file mode 100644 index 0000000000..a4823fea4d --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerConflictTests.java @@ -0,0 +1,162 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; + +class SchedulerConflictTests extends CommandTestBase { + @Test + void compositionsCannotAwaitConflictingCommands() { + var mech = new Mechanism("The Mechanism", m_scheduler); + + var group = + Command.noRequirements() + .executing( + co -> { + co.awaitAll( + mech.run(Coroutine::park).named("First"), + mech.run(Coroutine::park).named("Second")); + }) + .named("Group"); + + m_scheduler.schedule(group); + + // Running should attempt to schedule multiple conflicting commands + var exception = assertThrows(IllegalArgumentException.class, m_scheduler::run); + assertEquals( + "Commands running in parallel cannot share requirements: " + + "First and Second both require The Mechanism", + exception.getMessage()); + } + + @Test + void innerCommandMayInterruptOtherInnerCommand() { + var mechanism = new Mechanism("The mechanism", m_scheduler); + var firstRan = new AtomicBoolean(false); + var secondRan = new AtomicBoolean(false); + + var first = + mechanism + .run( + c -> { + firstRan.set(true); + c.park(); + }) + .named("First"); + + var second = + mechanism + .run( + c -> { + secondRan.set(true); + c.park(); + }) + .named("Second"); + + var group = + Command.noRequirements() + .executing( + co -> { + co.fork(first); + co.fork(second); + co.park(); + }) + .named("Group"); + + m_scheduler.schedule(group); + m_scheduler.run(); + + assertTrue(firstRan.get(), "First child should have run to a yield point"); + assertTrue(secondRan.get(), "Second child should have run to a yield point"); + assertFalse( + m_scheduler.isScheduledOrRunning(first), "First child should have been interrupted"); + assertTrue(m_scheduler.isRunning(second), "Second child should still be running"); + assertTrue(m_scheduler.isRunning(group), "Group should still be running"); + } + + @Test + void nestedOneShotCompositionsAllRunInOneCycle() { + var runs = new AtomicInteger(0); + Supplier makeOneShot = + () -> Command.noRequirements().executing(_c -> runs.incrementAndGet()).named("One Shot"); + var command = + Command.noRequirements() + .executing( + co -> { + co.fork(makeOneShot.get()); + co.fork(makeOneShot.get()); + co.fork( + Command.noRequirements() + .executing(inner -> inner.fork(makeOneShot.get())) + .named("Inner")); + co.fork( + Command.noRequirements() + .executing( + co2 -> { + co2.fork(makeOneShot.get()); + co2.fork( + Command.noRequirements() + .executing( + co3 -> { + co3.fork(makeOneShot.get()); + }) + .named("3")); + }) + .named("2")); + }) + .named("Command"); + + m_scheduler.schedule(command); + m_scheduler.run(); + assertEquals(5, runs.get(), "All oneshot commands should have run"); + assertFalse(m_scheduler.isRunning(command), "Command should have exited after one cycle"); + } + + @Test + void childConflictsWithHigherPriorityTopLevel() { + var mechanism = new Mechanism("mechanism", m_scheduler); + var top = mechanism.run(Coroutine::park).withPriority(10).named("Top"); + + // Child conflicts with and is lower priority than the Top command + // It should not be scheduled, and the parent command should exit immediately + var child = mechanism.run(Coroutine::park).named("Child"); + var parent = Command.noRequirements().executing(co -> co.await(child)).named("Parent"); + + m_scheduler.schedule(top); + m_scheduler.schedule(parent); + m_scheduler.run(); + + assertTrue(m_scheduler.isRunning(top), "Top command should not have been interrupted"); + assertFalse(m_scheduler.isRunning(child), "Conflicting child should not have run"); + assertFalse(m_scheduler.isRunning(parent), "Parent of conflicting child should have exited"); + } + + @Test + void childConflictsWithLowerPriorityTopLevel() { + var mechanism = new Mechanism("mechanism", m_scheduler); + var top = mechanism.run(Coroutine::park).withPriority(-10).named("Top"); + + // Child conflicts with and is higher priority than the Top command + // It should be scheduled, and the top command should be interrupted + var child = mechanism.run(Coroutine::park).named("Child"); + var parent = Command.noRequirements().executing(co -> co.await(child)).named("Parent"); + + m_scheduler.schedule(top); + m_scheduler.schedule(parent); + m_scheduler.run(); + + assertFalse(m_scheduler.isRunning(top), "Top command should have been interrupted"); + assertTrue(m_scheduler.isRunning(child), "Conflicting child should be running"); + assertTrue(m_scheduler.isRunning(parent), "Parent of conflicting child should be running"); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerErrorHandlingTests.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerErrorHandlingTests.java new file mode 100644 index 0000000000..758639dc42 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerErrorHandlingTests.java @@ -0,0 +1,208 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import org.junit.jupiter.api.Test; + +class SchedulerErrorHandlingTests extends CommandTestBase { + @Test + void errorDetection() { + var mechanism = new Mechanism("X", m_scheduler); + + var command = + mechanism + .run( + coroutine -> { + throw new RuntimeException("The exception"); + }) + .named("Bad Behavior"); + + new Trigger(m_scheduler, () -> true).onTrue(command); + + var e = assertThrows(RuntimeException.class, m_scheduler::run); + assertEquals("The exception", e.getMessage()); + + assertEquals( + "org.wpilib.commands3.SchedulerErrorHandlingTests", e.getStackTrace()[0].getClassName()); + assertEquals("lambda$errorDetection$0", e.getStackTrace()[0].getMethodName()); + + assertEquals("=== Command Binding Trace ===", e.getStackTrace()[2].getClassName()); + + assertEquals(getClass().getName(), e.getStackTrace()[3].getClassName()); + assertEquals("errorDetection", e.getStackTrace()[3].getMethodName()); + } + + @Test + void nestedErrorDetection() { + var command = + Command.noRequirements() + .executing( + co -> { + co.await( + Command.noRequirements() + .executing( + c2 -> { + new Trigger(m_scheduler, () -> true) + .onTrue( + Command.noRequirements() + .executing( + c3 -> { + // Throws IndexOutOfBoundsException + var unused = new ArrayList<>(0).get(-1); + }) + .named("Throws IndexOutOfBounds")); + c2.park(); + }) + .named("Schedules With Trigger")); + }) + .named("Schedules Directly"); + + m_scheduler.schedule(command); + + // The first run sets up the trigger, but does not fire + // The second run will fire the trigger and cause the inner command to run and throw + m_scheduler.run(); + + var e = assertThrows(IndexOutOfBoundsException.class, m_scheduler::run); + StackTraceElement[] stackTrace = e.getStackTrace(); + + assertEquals("Index -1 out of bounds for length 0", e.getMessage()); + int nestedIndex = 0; + for (; nestedIndex < stackTrace.length; nestedIndex++) { + if (stackTrace[nestedIndex].getClassName().equals(getClass().getName())) { + break; + } + } + + // user code trace for the scheduler run invocation (to `scheduler.run()` in the try block) + assertEquals("lambda$nestedErrorDetection$3", stackTrace[nestedIndex].getMethodName()); + assertEquals("assertThrows", stackTrace[nestedIndex + 1].getMethodName()); + + // user code trace for where the command was scheduled (the `.onTrue()` line) + assertEquals("=== Command Binding Trace ===", stackTrace[nestedIndex + 2].getClassName()); + assertEquals("lambda$nestedErrorDetection$4", stackTrace[nestedIndex + 3].getMethodName()); + assertEquals("lambda$nestedErrorDetection$5", stackTrace[nestedIndex + 4].getMethodName()); + assertEquals("nestedErrorDetection", stackTrace[nestedIndex + 5].getMethodName()); + } + + @Test + void commandEncounteringErrorCancelsChildren() { + var child = Command.noRequirements().executing(Coroutine::park).named("Child 1"); + var command = + Command.noRequirements() + .executing( + co -> { + co.fork(child); + throw new RuntimeException("The exception"); + }) + .named("Bad Behavior"); + + m_scheduler.schedule(command); + assertThrows(RuntimeException.class, m_scheduler::run); + assertFalse( + m_scheduler.isScheduledOrRunning(command), + "Command should have been removed from the scheduler"); + assertFalse( + m_scheduler.isScheduledOrRunning(child), + "Child should have been removed from the scheduler"); + } + + @Test + void childCommandEncounteringErrorCancelsParent() { + var child = + Command.noRequirements() + .executing( + co -> { + throw new RuntimeException("The exception"); // note: bubbles up to the parent + }) + .named("Child 1"); + var command = + Command.noRequirements() + .executing( + co -> { + co.await(child); + co.park(); // pretend other things would happen after the child + }) + .named("Parent"); + + m_scheduler.schedule(command); + assertThrows(RuntimeException.class, m_scheduler::run); + assertFalse( + m_scheduler.isRunning(command), + "Parent command should have been removed from the scheduler"); + assertFalse(m_scheduler.isRunning(child), "Child should have been removed from the scheduler"); + } + + @Test + @SuppressWarnings("PMD.CompareObjectsWithEquals") + void childCommandEncounteringErrorAfterRemountCancelsParent() { + var child = + Command.noRequirements() + .executing( + co -> { + co.yield(); + throw new RuntimeException("The exception"); // does not bubble up to the parent + }) + .named("Child 1"); + var command = + Command.noRequirements() + .executing( + co -> { + co.await(child); + co.park(); // pretend other things would happen after the child + }) + .named("Parent"); + + m_scheduler.schedule(command); + + // first run schedules the child and adds it to the running set + m_scheduler.run(); + + // second run encounters the error in the child + final var error = assertThrows(RuntimeException.class, m_scheduler::run); + assertFalse( + m_scheduler.isRunning(command), + "Parent command should have been removed from the scheduler"); + assertFalse(m_scheduler.isRunning(child), "Child should have been removed from the scheduler"); + + // Full event history + assertEquals(9, m_events.size()); + assertTrue( + m_events.get(0) instanceof SchedulerEvent.Scheduled s && s.command() == command, + "First event should be parent scheduled"); + assertTrue( + m_events.get(1) instanceof SchedulerEvent.Mounted m && m.command() == command, + "Second event should be parent mounted"); + assertTrue( + m_events.get(2) instanceof SchedulerEvent.Scheduled s && s.command() == child, + "Third event should be child scheduled"); + assertTrue( + m_events.get(3) instanceof SchedulerEvent.Mounted m && m.command() == child, + "Fourth event should be child mounted"); + assertTrue( + m_events.get(4) instanceof SchedulerEvent.Yielded y && y.command() == child, + "Fifth event should be child yielded"); + assertTrue( + m_events.get(5) instanceof SchedulerEvent.Yielded y && y.command() == command, + "Sixth event should be parent yielded"); + assertTrue( + m_events.get(6) instanceof SchedulerEvent.Mounted m && m.command() == child, + "Seventh event should be child remounted"); + assertTrue( + m_events.get(7) instanceof SchedulerEvent.CompletedWithError c + && c.command() == child + && c.error() == error, + "Eighth event should be child completed with error"); + assertTrue( + m_events.get(8) instanceof SchedulerEvent.Canceled c && c.command() == command, + "Ninth event should be parent canceled"); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerPriorityLevelTests.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerPriorityLevelTests.java new file mode 100644 index 0000000000..3e65aa8163 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerPriorityLevelTests.java @@ -0,0 +1,64 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class SchedulerPriorityLevelTests extends CommandTestBase { + @Test + void higherPriorityCancels() { + final var subsystem = new Mechanism("Subsystem", m_scheduler); + + final var lower = new PriorityCommand(-1000, subsystem); + final var higher = new PriorityCommand(+1000, subsystem); + + m_scheduler.schedule(lower); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(lower)); + + m_scheduler.schedule(higher); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(higher)); + assertFalse(m_scheduler.isRunning(lower)); + } + + @Test + void lowerPriorityDoesNotCancel() { + final var subsystem = new Mechanism("Subsystem", m_scheduler); + + final var lower = new PriorityCommand(-1000, subsystem); + final var higher = new PriorityCommand(+1000, subsystem); + + m_scheduler.schedule(higher); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(higher)); + + m_scheduler.schedule(lower); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(higher), "Higher priority command should still be running"); + assertFalse( + m_scheduler.isScheduledOrRunning(lower), "Lower priority command should not be running"); + } + + @Test + void samePriorityCancels() { + final var subsystem = new Mechanism("Subsystem", m_scheduler); + + final var first = new PriorityCommand(512, subsystem); + final var second = new PriorityCommand(512, subsystem); + + m_scheduler.schedule(first); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(first)); + + m_scheduler.schedule(second); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(second), "New command should be running"); + assertFalse(m_scheduler.isRunning(first), "Old command should be canceled"); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerSideloadFunctionTests.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerSideloadFunctionTests.java new file mode 100644 index 0000000000..958005e5e7 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerSideloadFunctionTests.java @@ -0,0 +1,100 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class SchedulerSideloadFunctionTests extends CommandTestBase { + @Test + void sideloadThrowingException() { + m_scheduler.sideload( + co -> { + throw new RuntimeException("Bang!"); + }); + + // An exception raised in a sideload function should bubble up + assertEquals( + "Bang!", assertThrowsExactly(RuntimeException.class, m_scheduler::run).getMessage()); + } + + @Test + void periodicSideload() { + AtomicInteger count = new AtomicInteger(0); + m_scheduler.addPeriodic(count::incrementAndGet); + assertEquals(0, count.get()); + + m_scheduler.run(); + assertEquals(1, count.get()); + + m_scheduler.run(); + assertEquals(2, count.get()); + } + + @Test + void sideloadSchedulingCommand() { + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + // one-shot sideload forks a command and immediately exits + m_scheduler.sideload(co -> co.fork(command)); + m_scheduler.run(); + assertTrue( + m_scheduler.isRunning(command), "command should have started and outlasted the sideload"); + } + + @Test + void childCommandEscapesViaSideload() { + var child = Command.noRequirements().executing(Coroutine::park).named("Child"); + var parent = + Command.noRequirements() + .executing( + parentCoroutine -> { + m_scheduler.sideload(sidelodCoroutine -> sidelodCoroutine.fork(child)); + }) + .named("Parent"); + + m_scheduler.schedule(parent); + m_scheduler.run(); + assertFalse(m_scheduler.isScheduledOrRunning(parent), "parent should have exited"); + assertFalse( + m_scheduler.isScheduledOrRunning(child), + "the sideload to schedule the child should not have run yet"); + + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(child), "child should have started running"); + } + + @Test + void sideloadCancelingCommand() { + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + m_scheduler.schedule(command); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command), "command should have started"); + + m_scheduler.sideload(co -> m_scheduler.cancel(command)); + assertTrue(m_scheduler.isRunning(command), "sideload should not have run yet"); + + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(command), "sideload should have canceled the command"); + } + + @Test + void sideloadAffectsStateForTriggerInSameCycle() { + AtomicBoolean signal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, signal::get); + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.onTrue(command); + m_scheduler.sideload(co -> signal.set(true)); + + m_scheduler.run(); + assertTrue(signal.get(), "Sideload should have run and set the signal"); + assertTrue(m_scheduler.isRunning(command), "Command should have started"); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTelemetryTests.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTelemetryTests.java new file mode 100644 index 0000000000..9c7f07bff4 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTelemetryTests.java @@ -0,0 +1,116 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class SchedulerTelemetryTests extends CommandTestBase { + @Test + void protobuf() { + var mech = new Mechanism("The mechanism", m_scheduler); + var parkCommand = mech.run(Coroutine::park).named("Park"); + var c3Command = mech.run(co -> co.await(parkCommand)).named("C3"); + var c2Command = mech.run(co -> co.await(c3Command)).named("C2"); + var group = mech.run(co -> co.await(c2Command)).named("Group"); + + m_scheduler.schedule(group); + m_scheduler.run(); + + var scheduledCommand1 = Command.noRequirements().executing(Coroutine::park).named("Command 1"); + var scheduledCommand2 = Command.noRequirements().executing(Coroutine::park).named("Command 2"); + m_scheduler.schedule(scheduledCommand1); + m_scheduler.schedule(scheduledCommand2); + + var message = Scheduler.proto.createMessage(); + Scheduler.proto.pack(message, m_scheduler); + var messageJson = message.toString(); + assertEquals( + """ + { + "lastTimeMs": %s, + "queuedCommands": [{ + "priority": 0, + "id": %s, + "name": "Command 1", + "requirements": [] + }, { + "priority": 0, + "id": %s, + "name": "Command 2", + "requirements": [] + }], + "runningCommands": [{ + "lastTimeMs": %s, + "totalTimeMs": %s, + "priority": 0, + "id": %s, + "name": "Group", + "requirements": [{ + "name": "The mechanism" + }] + }, { + "lastTimeMs": %s, + "totalTimeMs": %s, + "priority": 0, + "id": %s, + "parentId": %s, + "name": "C2", + "requirements": [{ + "name": "The mechanism" + }] + }, { + "lastTimeMs": %s, + "totalTimeMs": %s, + "priority": 0, + "id": %s, + "parentId": %s, + "name": "C3", + "requirements": [{ + "name": "The mechanism" + }] + }, { + "lastTimeMs": %s, + "totalTimeMs": %s, + "priority": 0, + "id": %s, + "parentId": %s, + "name": "Park", + "requirements": [{ + "name": "The mechanism" + }] + }] + }""" + .formatted( + // Scheduler data + m_scheduler.lastRuntimeMs(), + + // On deck commands + m_scheduler.runId(scheduledCommand1), + m_scheduler.runId(scheduledCommand2), + + // Running commands + m_scheduler.lastCommandRuntimeMs(group), + m_scheduler.totalRuntimeMs(group), + m_scheduler.runId(group), // id + // top-level command, no parent ID + + m_scheduler.lastCommandRuntimeMs(c2Command), + m_scheduler.totalRuntimeMs(c2Command), + m_scheduler.runId(c2Command), // id + m_scheduler.runId(group), // parent + m_scheduler.lastCommandRuntimeMs(c3Command), + m_scheduler.totalRuntimeMs(c3Command), + m_scheduler.runId(c3Command), // id + m_scheduler.runId(c2Command), // parent + m_scheduler.lastCommandRuntimeMs(parkCommand), + m_scheduler.totalRuntimeMs(parkCommand), + m_scheduler.runId(parkCommand), // id + m_scheduler.runId(c3Command) // parent + ), + messageJson); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTest.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTest.java new file mode 100644 index 0000000000..4d5fd92a7b --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTest.java @@ -0,0 +1,161 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class SchedulerTest extends CommandTestBase { + @Test + void basic() { + var enabled = new AtomicBoolean(false); + var ran = new AtomicBoolean(false); + var command = + Command.noRequirements() + .executing( + coroutine -> { + do { + coroutine.yield(); + } while (!enabled.get()); + ran.set(true); + }) + .named("Basic Command"); + + m_scheduler.schedule(command); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command), "Command should be running after being scheduled"); + + enabled.set(true); + m_scheduler.run(); + if (m_scheduler.isRunning(command)) { + fail("Command should no longer be running after awaiting its completion"); + } + + assertTrue(ran.get()); + } + + @Test + @SuppressWarnings("PMD.ImmutableField") // PMD bugs + void atomicity() { + var mechanism = + new Mechanism("X", m_scheduler) { + int m_x = 0; + }; + + // Launch 100 commands that each call `x++` 500 times. + // If commands run on different threads, the lack of atomic + // operations or locks will mean the final number will be + // less than the expected 50,000 + int numCommands = 100; + int iterations = 500; + + for (int cmdCount = 0; cmdCount < numCommands; cmdCount++) { + var command = + Command.noRequirements() + .executing( + coroutine -> { + for (int i = 0; i < iterations; i++) { + mechanism.m_x++; + coroutine.yield(); + } + }) + .named("CountCommand[" + cmdCount + "]"); + + m_scheduler.schedule(command); + } + + for (int i = 0; i < iterations; i++) { + m_scheduler.run(); + } + + assertEquals(numCommands * iterations, mechanism.m_x); + } + + @Test + @SuppressWarnings("PMD.ImmutableField") // PMD bugs + void runMechanism() { + var example = + new Mechanism("Counting", m_scheduler) { + int m_x = 0; + }; + + Command countToTen = + example + .run( + coroutine -> { + example.m_x = 0; + for (int i = 0; i < 10; i++) { + coroutine.yield(); + example.m_x++; + } + }) + .named("Count To Ten"); + + m_scheduler.schedule(countToTen); + for (int i = 0; i < 10; i++) { + m_scheduler.run(); + } + m_scheduler.run(); + + assertEquals(10, example.m_x); + } + + @Test + void compositionsDoNotNeedRequirements() { + var m1 = new Mechanism("M1", m_scheduler); + var m2 = new Mechanism("m2", m_scheduler); + + // the group has no requirements, but can schedule child commands that do + var group = + Command.noRequirements() + .executing( + co -> { + co.awaitAll( + m1.run(Coroutine::park).named("M1 Command"), + m2.run(Coroutine::park).named("M2 Command")); + }) + .named("Composition"); + + m_scheduler.schedule(group); + m_scheduler.run(); // start m1 and m2 commands + assertEquals(3, m_scheduler.getRunningCommands().size()); + } + + @Test + void nestedMechanisms() { + var superstructure = + new Mechanism("Superstructure", m_scheduler) { + private final Mechanism m_elevator = new Mechanism("Elevator", m_scheduler); + private final Mechanism m_arm = new Mechanism("Arm", m_scheduler); + + public Command superCommand() { + return run(co -> { + co.await(m_elevator.run(Coroutine::park).named("Elevator Subcommand")); + co.await(m_arm.run(Coroutine::park).named("Arm Subcommand")); + }) + .named("Super Command"); + } + }; + + m_scheduler.schedule(superstructure.superCommand()); + m_scheduler.run(); + assertEquals( + List.of(superstructure.m_arm.getDefaultCommand()), + superstructure.m_arm.getRunningCommands(), + "Arm should only be running its default command"); + + // Scheduling something that requires an in-use inner mechanism cancels the outer command + m_scheduler.schedule(superstructure.m_elevator.run(Coroutine::park).named("Conflict")); + + m_scheduler.run(); // schedules the default superstructure command + m_scheduler.run(); // starts running the default superstructure command + assertEquals(List.of(superstructure.getDefaultCommand()), superstructure.getRunningCommands()); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTimingTests.java b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTimingTests.java new file mode 100644 index 0000000000..1643c7777f --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SchedulerTimingTests.java @@ -0,0 +1,244 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static edu.wpi.first.units.Units.Microseconds; +import static edu.wpi.first.units.Units.Milliseconds; +import static edu.wpi.first.units.Units.Seconds; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.wpi.first.wpilibj.RobotController; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +class SchedulerTimingTests extends CommandTestBase { + @Test + void commandAwaitingItself() { + // This command deadlocks on itself. It's calling yield() in an infinite loop, which is + // equivalent to calling Coroutine.park(). No deleterious side effects other than stalling + // the command + AtomicReference commandRef = new AtomicReference<>(); + var command = + Command.noRequirements().executing(co -> co.await(commandRef.get())).named("Self Await"); + commandRef.set(command); + + m_scheduler.schedule(command); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command)); + } + + @Test + void commandDeadlock() { + AtomicReference parentRef = new AtomicReference<>(); + AtomicReference childRef = new AtomicReference<>(); + + // Deadlock scenario: + // parent starts, schedules child, then waits for child to exit + // child starts, waits for parent to exit + // + // Each successive run sees parent mount, check for child, then yield. + // Then sees child mount, check for parent, then also yield. + // This is like two threads spinwaiting for the other to exit. + // + // Externally canceling child allows parent to continue + // Externally canceling parent cancels both + var parent = Command.noRequirements().executing(co -> co.await(childRef.get())).named("Parent"); + var child = Command.noRequirements().executing(co -> co.await(parentRef.get())).named("Child"); + parentRef.set(parent); + childRef.set(child); + + m_scheduler.schedule(parent); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(parent)); + assertTrue(m_scheduler.isRunning(child)); + + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(parent)); + assertTrue(m_scheduler.isRunning(child)); + + m_scheduler.cancel(parent); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(parent)); + assertFalse(m_scheduler.isRunning(child)); + } + + @Test + void delayedCommandDeadlock() { + AtomicReference ref1 = new AtomicReference<>(); + AtomicReference ref2 = new AtomicReference<>(); + + AtomicBoolean command1CompletedNormally = new AtomicBoolean(false); + AtomicBoolean command2CompletedNormally = new AtomicBoolean(false); + + // Deadlock scenario: + // command1 starts, waits for command2 to exit + // command2 starts, waits for command1 to exit + // + // Each successive run sees command1 mount, check for command2, then yield. + // Then sees command2 mount, check for command1, then also yield. + // This is like two threads spinwaiting for the other to exit. + // + // Externally canceling either command allows the other to exit + var command1 = + Command.noRequirements() + .executing( + co -> { + co.yield(); + co.await(ref2.get()); + command1CompletedNormally.set(true); + }) + .named("Command 1"); + var command2 = + Command.noRequirements() + .executing( + co -> { + co.yield(); + co.await(ref1.get()); + command2CompletedNormally.set(true); + }) + .named("Command 2"); + ref1.set(command1); + ref2.set(command2); + + m_scheduler.schedule(command1); + m_scheduler.schedule(command2); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command1)); + assertTrue(m_scheduler.isRunning(command2)); + + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command1)); + assertTrue(m_scheduler.isRunning(command2)); + + m_scheduler.cancel(command2); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(command1)); + assertFalse(m_scheduler.isRunning(command2)); + assertTrue( + command1CompletedNormally.get(), + "Command 1 should have completed normally after command 2 stopped"); + assertFalse( + command2CompletedNormally.get(), + "Canceling command 2 should have stopped it before completing"); + } + + @Test + void forkedChildRunsOnce() { + AtomicInteger runCount = new AtomicInteger(0); + + var inner = + Command.noRequirements() + .executing( + co -> { + runCount.incrementAndGet(); + co.yield(); + + runCount.incrementAndGet(); + co.yield(); + }) + .named("Inner"); + + var outer = Command.noRequirements().executing(co -> co.await(inner)).named("Outer"); + m_scheduler.schedule(outer); + m_scheduler.run(); + + assertEquals(1, runCount.get()); + } + + @Test + void shortWaitWaitsOneLoop() { + AtomicLong time = new AtomicLong(0); + RobotController.setTimeSource(time::get); + + AtomicBoolean completedWait = new AtomicBoolean(false); + var command = + Command.noRequirements() + .executing( + co -> { + co.wait(Milliseconds.of(1)); + completedWait.set(true); + }) + .named("Short Wait"); + + m_scheduler.schedule(command); + m_scheduler.run(); + + // wait 1 full second (much longer than the wait period) + time.set((long) Seconds.of(1).in(Microseconds)); + m_scheduler.run(); + assertTrue( + completedWait.get(), + "Command with a short wait should have completed if its duration has elapsed between runs"); + } + + @Test + void shortWaitWaitsOneLoopWithFastPeriod() { + AtomicLong time = new AtomicLong(0); + RobotController.setTimeSource(time::get); + AtomicBoolean completedWait = new AtomicBoolean(false); + var command = + Command.noRequirements() + .executing( + co -> { + co.wait(Milliseconds.of(1)); + completedWait.set(true); + }) + .named("Short Wait"); + + m_scheduler.schedule(command); + m_scheduler.run(); + + // move forward by half the wait period + time.set((long) Milliseconds.of(0.5).in(Microseconds)); + m_scheduler.run(); + assertFalse(completedWait.get(), "Command should still be waiting for 1 ms to elapse"); + + // move forward by the rest of the wait period + time.set((long) Milliseconds.of(1).in(Microseconds)); + m_scheduler.run(); + assertTrue( + completedWait.get(), + "Command with a short wait should have completed if its duration has elapsed between runs"); + } + + @Test + void awaitingExitsImmediatelyWithoutAOneLoopDelay() { + AtomicInteger innerRuns = new AtomicInteger(0); + var inner = + Command.noRequirements() + .executing( + co -> { + // executed immediately when forked + innerRuns.incrementAndGet(); + co.yield(); + + // executed again on the next scheduler run, after the forking command runs + innerRuns.incrementAndGet(); + }) + .named("Inner"); + + var outer = Command.noRequirements().executing(co -> co.await(inner)).named("Outer"); + m_scheduler.schedule(outer); + + // First run: runs outer, forks inner, inner runs to its first yield, outer yields + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(inner)); + assertTrue(m_scheduler.isRunning(outer)); + assertEquals(1, innerRuns.get()); + + // Second run: runs inner to completion, runs outer, outer sees inner is complete and exits + // NOTE: If child commands ran AFTER their parents, then outer would not have exited here and + // would take another scheduler run to complete + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(inner)); + assertFalse(m_scheduler.isRunning(outer)); + assertEquals(2, innerRuns.get()); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/SequentialGroupTest.java b/commandsv3/src/test/java/org/wpilib/commands3/SequentialGroupTest.java new file mode 100644 index 0000000000..4481ee8181 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/SequentialGroupTest.java @@ -0,0 +1,82 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class SequentialGroupTest extends CommandTestBase { + @Test + void single() { + var command = Command.noRequirements().executing(Coroutine::yield).named("The Command"); + + var sequence = new SequentialGroup("The Sequence", List.of(command)); + m_scheduler.schedule(sequence); + + // First run - the composed command starts and yields; sequence yields + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(sequence)); + assertTrue(m_scheduler.isRunning(command)); + + // Second run - the composed command completes; sequence sees its completion and exits + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(sequence)); + assertFalse(m_scheduler.isRunning(command)); + } + + @Test + void twoCommands() { + var c1 = Command.noRequirements().executing(Coroutine::yield).named("C1"); + var c2 = Command.noRequirements().executing(Coroutine::yield).named("C2"); + + var sequence = new SequentialGroup("C1 > C2", List.of(c1, c2)); + m_scheduler.schedule(sequence); + + // First run - c1 is scheduled and starts + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(sequence), "Sequence should be running"); + assertTrue(m_scheduler.isRunning(c1), "Starting the sequence should start the first command"); + assertFalse( + m_scheduler.isScheduledOrRunning(c2), + "The second command should still be pending completion of the first command"); + + // Second run - c1 completes, sequence sees it finish, schedules c2 + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(sequence)); + assertFalse(m_scheduler.isRunning(c1), "First command should have completed"); + assertTrue( + m_scheduler.isScheduledOrRunning(c2), "Second command should not start in the same cycle"); + + // Third run - c2 completes, sequence sees it finish, exits + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(sequence)); + assertFalse(m_scheduler.isRunning(c2), "Second command should have started"); + } + + @Test + void inheritsRequirements() { + var mech1 = new Mechanism("Mech 1", m_scheduler); + var mech2 = new Mechanism("Mech 2", m_scheduler); + var command1 = mech1.run(Coroutine::park).named("Command 1"); + var command2 = mech2.run(Coroutine::park).named("Command 2"); + var sequence = new SequentialGroup("Sequence", List.of(command1, command2)); + assertEquals(Set.of(mech1, mech2), sequence.requirements(), "Requirements were not inherited"); + } + + @Test + void inheritsPriority() { + var mech1 = new Mechanism("Mech 1", m_scheduler); + var mech2 = new Mechanism("Mech 2", m_scheduler); + var command1 = mech1.run(Coroutine::park).withPriority(100).named("Command 1"); + var command2 = mech2.run(Coroutine::park).withPriority(200).named("Command 2"); + var sequence = new SequentialGroup("Sequence", List.of(command1, command2)); + assertEquals(200, sequence.priority(), "Priority was not inherited"); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/StagedCommandBuilderTest.java b/commandsv3/src/test/java/org/wpilib/commands3/StagedCommandBuilderTest.java new file mode 100644 index 0000000000..44fef542a8 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/StagedCommandBuilderTest.java @@ -0,0 +1,254 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StagedCommandBuilderTest { + private static final Runnable no_op = () -> {}; + + private Mechanism m_mech1; + private Mechanism m_mech2; + + @BeforeEach + void setUp() { + Scheduler scheduler = Scheduler.createIndependentScheduler(); + m_mech1 = new Mechanism("Mech 1", scheduler); + m_mech2 = new Mechanism("Mech 2", scheduler); + } + + // The next two tests are to check that various forms of builder usage are able to compile. + + @Test + void streamlined() { + Command command = + new StagedCommandBuilder() + .noRequirements() + .executing(Coroutine::park) + .until(() -> false) + .named("Name"); + + assertEquals("Name", command.name()); + } + + @Test + void allOptions() { + var mech = new Mechanism("Mech", Scheduler.createIndependentScheduler()); + + Command command = + new StagedCommandBuilder() + .noRequirements() + .requiring(mech) + .requiring(mech, mech) + .requiring(List.of(mech)) + .executing(Coroutine::park) + .whenCanceled(no_op) + .until(() -> false) + .withPriority(10) + .named("Name"); + + assertEquals("Name", command.name()); + } + + @Test + void starting_noRequirements_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var ignored = builder.noRequirements().executing(c -> {}).named("cmd"); + + var err = assertThrows(IllegalStateException.class, builder::noRequirements); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void starting_requiringVarargs_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var ignored = builder.noRequirements().executing(c -> {}).named("cmd"); + + var err = assertThrows(IllegalStateException.class, () -> builder.requiring(m_mech1, m_mech2)); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void starting_requiringCollection_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var ignored = builder.noRequirements().executing(c -> {}).named("cmd"); + + var err = + assertThrows( + IllegalStateException.class, () -> builder.requiring(List.of(m_mech1, m_mech2))); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void requirements_requiringSingle_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var reqStage = builder.noRequirements(); + var ignored = reqStage.executing(c -> {}).named("cmd"); + + var err = assertThrows(IllegalStateException.class, () -> reqStage.requiring(m_mech1)); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void requirements_requiringVarargs_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var reqStage = builder.noRequirements(); + var ignored = reqStage.executing(Coroutine::park).named("cmd"); + + var err = assertThrows(IllegalStateException.class, () -> reqStage.requiring(m_mech1, m_mech2)); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void requirements_requiringCollection_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var reqStage = builder.noRequirements(); + var ignored = reqStage.executing(Coroutine::park).named("cmd"); + + var err = assertThrows(IllegalStateException.class, () -> reqStage.requiring(List.of(m_mech1))); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void requirements_executing_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var reqStage = builder.noRequirements(); + var ignored = reqStage.executing(c -> {}).named("cmd"); + + Consumer impl = Coroutine::park; + var err = assertThrows(IllegalStateException.class, () -> reqStage.executing(impl)); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void execution_whenCanceled_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var execStage = builder.noRequirements().executing(c -> {}); + var ignored = execStage.named("cmd"); + + var err = assertThrows(IllegalStateException.class, () -> execStage.whenCanceled(() -> {})); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void execution_withPriority_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var execStage = builder.noRequirements().executing(c -> {}); + var ignored = execStage.named("cmd"); + + var err = assertThrows(IllegalStateException.class, () -> execStage.withPriority(7)); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void execution_until_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var execStage = builder.noRequirements().executing(c -> {}); + var ignored = execStage.named("cmd"); + + BooleanSupplier endCondition = () -> true; + var err = assertThrows(IllegalStateException.class, () -> execStage.until(endCondition)); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void execution_named_throwsAfterBuild() { + var builder = new StagedCommandBuilder(); + var execStage = builder.noRequirements().executing(c -> {}); + var ignored = execStage.named("cmd"); + + var err = assertThrows(IllegalStateException.class, () -> execStage.named("other")); + assertEquals("Command builders cannot be reused", err.getMessage()); + } + + @Test + void starting_requiringVarargs_nullFirstRequirement_throwsNPE() { + var builder = new StagedCommandBuilder(); + assertThrows(NullPointerException.class, () -> builder.requiring(null, m_mech2)); + } + + @Test + void starting_requiringVarargs_nullArray_throwsNPE() { + var builder = new StagedCommandBuilder(); + assertThrows(NullPointerException.class, () -> builder.requiring(m_mech1, (Mechanism[]) null)); + } + + @Test + void starting_requiringVarargs_nullInExtra_throwsNPE() { + var builder = new StagedCommandBuilder(); + assertThrows(NullPointerException.class, () -> builder.requiring(m_mech1, m_mech2, null)); + } + + @Test + void starting_requiringCollection_nullCollection_throwsNPE() { + var builder = new StagedCommandBuilder(); + assertThrows(NullPointerException.class, () -> builder.requiring((Collection) null)); + } + + @Test + void starting_requiringCollection_nullElement_throwsNPE() { + var builder = new StagedCommandBuilder(); + var listWithNull = Arrays.asList(m_mech1, null, m_mech2); // Arrays.asList allows nulls + assertThrows(NullPointerException.class, () -> builder.requiring(listWithNull)); + } + + @Test + void requirements_requiringSingle_null_throwsNPE() { + var req = new StagedCommandBuilder().noRequirements(); + assertThrows(NullPointerException.class, () -> req.requiring((Mechanism) null)); + } + + @Test + void requirements_requiringVarargs_nullFirstRequirement_throwsNPE() { + var req = new StagedCommandBuilder().noRequirements(); + assertThrows(NullPointerException.class, () -> req.requiring(null, m_mech2)); + } + + @Test + void requirements_requiringVarargs_nullArray_throwsNPE() { + var req = new StagedCommandBuilder().noRequirements(); + assertThrows(NullPointerException.class, () -> req.requiring(m_mech1, (Mechanism[]) null)); + } + + @Test + void requirements_requiringVarargs_nullInExtra_throwsNPE() { + var req = new StagedCommandBuilder().noRequirements(); + assertThrows(NullPointerException.class, () -> req.requiring(m_mech1, m_mech2, null)); + } + + @Test + void requirements_requiringCollection_nullCollection_throwsNPE() { + var req = new StagedCommandBuilder().noRequirements(); + assertThrows(NullPointerException.class, () -> req.requiring((Collection) null)); + } + + @Test + void requirements_requiringCollection_nullElement_throwsNPE() { + var req = new StagedCommandBuilder().noRequirements(); + var listWithNull = Arrays.asList(m_mech1, null); // Arrays.asList allows nulls + assertThrows(NullPointerException.class, () -> req.requiring(listWithNull)); + } + + @Test + void requirements_executing_nullImpl_throwsNPE() { + var req = new StagedCommandBuilder().noRequirements(); + assertThrows(NullPointerException.class, () -> req.executing(null)); + } + + @Test + void execution_named_nullName_throwsNPE() { + var exec = new StagedCommandBuilder().noRequirements().executing(c -> {}); + assertThrows(NullPointerException.class, () -> exec.named(null)); + } +} diff --git a/commandsv3/src/test/java/org/wpilib/commands3/TriggerTest.java b/commandsv3/src/test/java/org/wpilib/commands3/TriggerTest.java new file mode 100644 index 0000000000..fdc5b5dce5 --- /dev/null +++ b/commandsv3/src/test/java/org/wpilib/commands3/TriggerTest.java @@ -0,0 +1,247 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.commands3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class TriggerTest extends CommandTestBase { + @Test + void onTrue() { + var signal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, signal::get); + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.onTrue(command); + + signal.set(true); + m_scheduler.run(); + + assertTrue(m_scheduler.isRunning(command), "Command was not scheduled on rising edge"); + + signal.set(false); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command), "Command should still be running on falling edge"); + } + + @Test + void onFalse() { + var signal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, signal::get); + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.onFalse(command); + + m_scheduler.run(); + assertTrue( + m_scheduler.isRunning(command), "Command should be scheduled when signal starts low"); + + signal.set(true); + m_scheduler.run(); + assertTrue( + m_scheduler.isRunning(command), "Command should still be running rising falling edge"); + } + + @Test + void whileTrue() { + var signal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, signal::get); + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.whileTrue(command); + + signal.set(true); + m_scheduler.run(); + + assertTrue(m_scheduler.isRunning(command), "Command was not scheduled on rising edge"); + + signal.set(false); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(command), "Command should be canceled on falling edge"); + } + + @Test + void whileFalse() { + var signal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, signal::get); + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.whileFalse(command); + + m_scheduler.run(); + assertTrue( + m_scheduler.isRunning(command), "Command should be scheduled when signal starts low"); + + signal.set(true); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(command), "Command should be canceled on rising edge"); + } + + @Test + void toggleOnTrue() { + var signal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, signal::get); + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.toggleOnTrue(command); + + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(command)); + + signal.set(true); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command)); + + signal.set(false); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command)); + + signal.set(true); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(command)); + } + + @Test + void toggleOnFalse() { + var signal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, signal::get); + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.toggleOnFalse(command); + + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command)); + + signal.set(true); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command)); + + signal.set(false); + m_scheduler.run(); + assertFalse(m_scheduler.isRunning(command)); + } + + @Test + void commandScoping() { + var innerRan = new AtomicBoolean(false); + var innerSignal = new AtomicBoolean(false); + + var inner = + Command.noRequirements() + .executing( + co -> { + while (true) { + innerRan.set(true); + co.park(); + } + }) + .named("Inner"); + + var outer = + Command.noRequirements() + .executing( + co -> { + new Trigger(m_scheduler, innerSignal::get).onTrue(inner); + // If we yield, then the outer command exits and immediately cancels the + // on-deck inner command before it can run + co.park(); + }) + .named("Outer"); + + m_scheduler.schedule(outer); + m_scheduler.run(); + assertFalse(innerRan.get(), "The bound command should not run before the signal is set"); + + innerSignal.set(true); + m_scheduler.run(); + assertTrue(innerRan.get(), "The nested trigger should have run the bound command"); + + innerRan.set(false); + m_scheduler.run(); + assertFalse(innerRan.get(), "Trigger should not have fired again"); + } + + @Test + void scopeGoingInactiveCancelsBoundCommand() { + var activeScope = new AtomicBoolean(true); + BindingScope scope = activeScope::get; + + var triggerSignal = new AtomicBoolean(false); + var trigger = new Trigger(m_scheduler, triggerSignal::get); + + var command = Command.noRequirements().executing(Coroutine::park).named("Command"); + trigger.addBinding(scope, BindingType.RUN_WHILE_HIGH, command); + + triggerSignal.set(true); + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(command), "Command should have started when triggered"); + + activeScope.set(false); + m_scheduler.run(); + assertFalse( + m_scheduler.isRunning(command), + "Command should have been canceled when scope became inactive"); + } + + // The scheduler lifecycle polls triggers at the start of `run()` + // Even though the trigger condition is set, the command exits and the trigger's scope goes + // inactive before the next `run()` call can poll the trigger + @Test + void triggerFromExitingCommandDoesNotFire() { + var condition = new AtomicBoolean(false); + var triggeredCommandRan = new AtomicBoolean(false); + + var inner = + Command.noRequirements() + .executing( + co -> { + triggeredCommandRan.set(true); + co.park(); + }) + .named("Inner"); + + var awaited = + Command.noRequirements() + .executing( + co -> { + co.yield(); + condition.set(true); + }) + .named("Awaited"); + + var outer = + Command.noRequirements() + .executing( + co -> { + new Trigger(m_scheduler, condition::get).onTrue(inner); + co.await(awaited); + }) + .named("Outer"); + + m_scheduler.schedule(outer); + + // First run: schedules `awaited`, yields + m_scheduler.run(); + assertTrue(m_scheduler.isRunning(outer)); + assertTrue(m_scheduler.isRunning(awaited)); + assertEquals( + List.of("Outer", "Awaited"), + m_scheduler.getRunningCommands().stream().map(Command::name).toList()); + + // Second run: `awaited` resumes, sets the condition, exits. `outer` exits its final `yield` + // and will exit on the next run. The trigger condition has been set, but the trigger is checked + // on the next call to `run()` + m_scheduler.run(); + assertEquals(List.of(), m_scheduler.getRunningCommands().stream().map(Command::name).toList()); + assertTrue(condition.get(), "Condition wasn't set"); + assertFalse(triggeredCommandRan.get(), "Command was unexpectedly triggered"); + + // Third run: trigger binding fires (outside a running command) and queues up `inner`. + // However, the inner command's lifetime is bound to `outer`, and is immediately canceled before + // it can run when the outer command exits. + m_scheduler.run(); + assertEquals(List.of(), m_scheduler.getRunningCommands().stream().map(Command::name).toList()); + assertFalse(triggeredCommandRan.get(), "Command was unexpectedly triggered"); + } +} diff --git a/design-docs/commands-v3.md b/design-docs/commands-v3.md new file mode 100644 index 0000000000..10e77699f8 --- /dev/null +++ b/design-docs/commands-v3.md @@ -0,0 +1,420 @@ +# WPILib Commands Version 3 + +[TOC] + +## Problem Statement {#problem-statement} + +The WPILib command framework uses cooperative multitasking to execute many commands concurrently; +each command has its own `execute()` function called periodically by the scheduler’s `run()` loop. +This API share has a steep learning curve, since new students learn looping by writing their own +`for` or `while` loops - not by using a framework that does the looping for them. Programmers +unfamiliar with the command framework commonly run into a problem where they write a while loop +inside their command’s `execute()` function, thus stalling the scheduler (and, by extension, the +entire robot program) by never ceding control until its end condition is met. + +Version 3 of the commands framework proposes to fix this issue by leveraging a new API introduced in +Java 21: continuations. While they are currently an internal JDK API used to underpin virtual thread +runtimes, we can still access and use it. Continuations are used by the JDK to take and save a +snapshot of a mounted function’s call stack and registers, and can be remounted later to continue +from where it left off; a canceled command would simply never be remounted, and its continuation +left for the garbage collector to clean up. + +## Goals + +### Make command logic look like normal functions + +The hardest part of learning the v1 and v2 command frameworks is the splitting out of logic into +separate init/execute/end/isFinished methods. Coroutines allow us to merge those all back into a +single function: + +```java +void commandBody(Coroutine coroutine) { + initialize(); + while (!isFinished()) { + execute(); + coroutine.yield(); + } + end(); +} +``` + +Coroutines allow commands to be normal methods that yield control back to the scheduler mid-method; +the scheduler will issue a continuation to each command, and the JVM can unmount a continuation - +saving the stack and registers - which can be remounted later. **The primary goal of the new +framework is to allow for commands to be written as normal methods by taking advantage of this +mount/unmount feature.** Everything else is focused on quality of life improvements to the +framework. + +### Forced Naming + +The v2 commands framework makes naming opt-in; it is very easy to forget to set a name on a command +in a factory method, making telemetry less useful than it should be; seeing a +“SequentialCommandGroup” in logs isn’t very useful. The v3 framework will make command names +required in order for commands to be constructed. See [Telemetry](#telemetry) for details. + +### Uncommanded Behavior + +Command groups - or, more generally, commands that schedule other commands - need to own all +mechanisms needed by all their inner commands. However, if they don’t *directly* control a mechanism, +then they will still own the mechanism whenever they’re not running an inner command that uses it, +causing the mechanism to be in an *uncommanded state*. This can cause unexpected behavior with +parallel groups where inner commands don’t all end at the same time, and especially with sequential +groups. For example, something as seemingly basic as `elevator.toL4().andThen(coral.score())` would +*own* the elevator and coral mechanisms for the entire sequence, but not control the elevator during +the coral scoring section, nor control the coral scoring mechanism while the elevator is moving. + +The v2 framework worked around this problem with so-called proxy commands, which have no +requirements of their own and simply schedule and await the *real* command. This removed the +requirement for their used subsystems from the parent group, and allowed for the subsystem’s default +command to run after the proxied command completed. It also meant that *every* command that required +that subsystem would also need to be proxied; otherwise, the group would still have ownership of the +subsystem, and the proxied command would interrupt it (and thus itself). + +The v3 framework allows nested commands to use mechanisms not required by the parent command; +however, the built-in parallel and sequential groups will take full ownership of all mechanisms +required by their nested commands for safety and to keep behavior parity with previous command +frameworks. + +```java +public Command outerCommand() { + // The outer command only requires the outer mechanism + return run(coroutine -> { + // But can schedule a command that requires any other mechanism + coroutine.await(innerMechanism.innerCommand()); + // And only requires that inner mechanism while its command runs + coroutine.await(otherMechanism().otherCommand()); + }).named("Outer"); +} +``` + +Scheduling a command that requires an inner mechanism will also cancel its parent, even if the parent +does not require that inner mechanism. Using the above example, directly scheduling a command that +requires `innerMechanism` will cancel a running `outerCommand` - but only if `outerCommand` is +*currently using* the inner mechanism at the time. + +**Effectively, all child commands in v3 are "proxied"**, using the v2 framework's definition, unless +using the built-in ParallelGroup and Sequence compositions or explicitly adding child command +requirements to the parent. However, child commands _cannot_ interrupt their parent, even if they +share requirements, unlike proxy commands in v2. + +### Priority Levels + +Priority levels allow for finer-grained control over when commands should be interruptible than a +binary interrupt/ignore interrupt flag. This can be particularly helpful for LED control, where +teams use lights to indicate robot activity or error states with error indicators taking priority. + +### Suspend/Resume + +Allow commands to be temporarily paused while a higher priority or interrupting command runs, then +be automatically resumed after that higher priority command finishes. Note that resuming commands +must check for a condition to determine if they should still be running, since the time between +pause and resume can be arbitrarily long. + +Suspending a command will suspend all its children, regardless of if those children have declared +themselves suspendable or not. Likewise, cancelling a non-suspendable command will also cancel all +its children, even if some of those children are suspendable. + +Suspending a command that already has suspended children will still suspend everything; however, +upon resume, those suspended children will remain suspended. + +### Inner Triggers + +Teams often want to use triggers only within the scope of certain commands. For example, running an +autonomous mode that follows a particular path, and do certain actions when various points along the +path are reached, but not have those actions be bound to globally visible triggers that may fire at +other times during a match. In v2, such triggers need to be composed either a with a function that +checks if a valid autonomous mode is running or with the same trigger that can cause that routine to +run. This leads to many, many triggers being defined in robot programs and makes it difficult to +understand when all those triggers may be used. + +The v3 framework will track the scopes in which trigger bindings are created, and automatically +delete those bindings (and cancel any running commands bound to them) when their scopes become +inactive. Users who manually schedule a command create a binding in a "global" scope that is always +active. Binding commands to a trigger, however, will use whatever the narrowest available scope is +at the time - creating a binding inside a running command will be scoped to that command's lifetime; +creating a binding outside a command (for example, in a main Robot class constructor) will also use +the "global" scope. + +```java +void bindDriveButtons() { + // Globally bound and will always be active + controller.a().onTrue(...) +} + +Trigger atScoringPosition = new Trigger(() -> getPosition().isNear(kScoringPosition)); + +Command autonomous() { + return Command.noRequirements().executing(coroutine -> { + // This binding only exists while the autonomous command is running + atScoringPosition.onTrue(score()); + + coroutine.await(driveToScoringPosition()); + }).named("Autonomous"); +} +``` + +### Improved Transparency + +The v2 commands framework only runs top-level commands in the scheduler; commands nested within a +composition like a sequential group or a simple `withTimeout` decoration are hidden from the +scheduler and do not appear in its sendable implementation, making telemetry difficult. A +“SequentialCommandGroup” in logs is less useful than “Wait 10 seconds -> Drive Forward”. Command +groups will automatically include the names of all their constituent commands, with options to +override when desired. + +The v3 framework will also provide a map of what command each mechanism is owned by. The v2 framework +only provides a list of the names of the running commands, without mapping those to subsystems. This +also makes telemetry difficult, since the order of commands in the list does not correspond with +subsystems; a command at a particular index may require different subsystems than a different +command at that same index that’s running at a later point in time, making data analysis in +AdvantageScope difficult since we can’t rely on consistent ordering. + +Telemetry will send lists of the on-deck and running commands. Commands in those lists will appear +as an ID number, parent command ID (if any; top level commands have no parent), name, names of +the required mechanisms, priority level, last time to process, and total processing time. Separate +runs of the same command object have different ID numbers and processing time data. The total time +spent in the scheduler loop will also be included, but not the aggregate total of _all_ loops. + +## Non-Goals + +### Preemptive Multitasking + +It is not a goal of the v3 framework to offer preemptive multitasking (that is, having the scheduler +forcibly suspend a long-running command in order to run another queued one). This would require +support from the JDK for allowing custom virtual thread schedulers, which it does not currently +offer, and a custom thread scheduler would be complicated and difficult to maintain. + +### Multithreading Support + +Like the previous iterations of the command-based framework, v3 will be designed for a +single-threaded environment. All commands will be run by a single thread, which is expected to be +the same thread on which commands are scheduled and canceled via triggers. No guarantees are made +for stability or proper functioning if used in a multithreaded environment. + +### Unbounded use of coroutines + +Coroutines are intended to be used within the context of a running command or sideloaded periodic +function, and rely on their backing continuations to be mounted in order to run. Using a coroutine +outside its command makes no sense, and an error will be thrown if attempting to do so: + +```java +Coroutine coroutine; +var badCommand = Command.noRequirements().executing(co -> { + coroutine = co; +}).named("Do not do this"); + +Scheduler.getInstance().schedule(badCommand); +Scheduler.getInstance().run(); + +// Doing anything with a captured coroutine will throw an error +co.fork(...); // IllegalStateException +co.yield(); // IllegalStateException +``` + +## Implementation Details + +### Nomenclature + +**Schedule**: Adding a command to a queue in the scheduler, requesting that that command start +running in the next call to the scheduler’s `run()` method. + +**Mount**: Make a command active by resuming its stack and executing it on the scheduler’s thread. + +**Unmount**: Freeze a command’s stack and state and remove it from the scheduler’s thread. An +unmounted command may be re-mounted in the future. + +**Run**: Adding a command to the scheduler and have it begin executing. Every unfinished command +will be run by the scheduler until they reach a `Coroutine.yield()` call, at which point the command +is unmounted and the next command can begin. + +**Cancel**: Request that a command stop running. A command that is scheduled and hasn’t yet started +running will be removed from the queue of scheduled commands. Cancellation requests are handled at +the end of each `run() `invocation. Cancelled commands are unmounted and will not be re-mounted; +they must be rescheduled and be issued new carrier coroutines. + +**Interrupt**: Scheduling a command that requires one or more mechanisms already in use by a running +command will interrupt and cancel that running command, so long as the running command has an equal +or lower priority level. Higher-priority commands cannot be interrupted by lower-priority ones. + +### Coroutines and Continuations + +`Continuation` and `ContinuationScope` are JDK-internal classes that manage stack saving and +loading (“unmounting” and “mounting”, respectively). Mounting a continuation essentially means +placing the continuation’s saved stack on top of the current stack. Calling +`Continuation.yield(ContinuationScope)` pauses the code that the continuation is evaluating and +cedes control back to the caller, which can then unmount the continuation. The scheduler uses this +by looping over every running command, mounting the continuation for the command, running the +command - which eventually either returns or calls `coroutine.yield()` - then unmounts the +continuation and moves on to the next command in the list. + +Coroutines are essentially wrappers around a continuation primitive that allows additional access to +the scheduler and provides async/await-like functionality. For example, `Coroutine.await(Command)` +will schedule a given command and call `yield()` in a loop until that command has completed +execution (note: this does *not* naively call `command.run(coroutine)`, which would hide the inner +command from the scheduler and potentially sidestep requirement mutexing) + +### Command Function Bodies + +Command logic lives in a single function `run` that accepts a `Coroutine` object argument. All +yielding is done via that coroutine, as is any management of nested commands (scheduling, awaiting, +cancelling, etc). + +The coroutine’s `yield()` method always returns `true`. When a command is cancelled, it is removed +from the set of running commands and its carrier objects left to be garbage collected. An +`onCancel()` method on the cancelled command will be invoked to let the command do any necessary +cleanup; this is needed because the command body will not be re-mounted and run, so a separate +function is used. Command builders allow this to be specified with a `whenCancelled(Runnable)` step. + +### Command Lifetimes + +All commands are managed and executed by the scheduler. No commands should manually run their inner +commands (which is what the old sequential and parallel groups did), since that hides those inner +commands from the scheduler and telemetry. However, this is not prohibited, nor would there be a way +to detect it. + +Inner commands are bound to the lifetime of their parents. Cancelling a higher-level command must +also cancel all the commands it scheduled (and so on down the hierarchy). Suspending a higher-level +command must likewise also suspend all inner commands, and resuming the higher-level command must +resume all suspended inner commands. Inner commands cannot be resumed before their parent command, +but may be suspended while the parent command still runs. + +When a command is running, the scheduler tracks in a wrapper CommandState object that includes the +command, the command that scheduled it (if applicable), and the coroutine object carrying the +command. + +Each command run has its own coroutine to back its execution. When a command finishes, its backing +continuation is done, and the command is removed from the scheduler. Any unfinished inner commands +are cancelled at this point. + +### Factory Methods and Builders + +In contrast with v2’s decorator API that returns a new command object for each modification (which +had some issues with naming and making telemetry difficult with deeply nested wrapper objects), v3 +uses builder objects that allow configuration to be set before creating a single command at the end. +Builders are defined in stages, with a different type representing each stage, in order to leverage +the type system to prevent users from creating invalid commands and only discovering so at runtime. +Failure to apply required configuration options (eg the main command function or a name) will leave +users with an intermediate-stage builder that cannot be used to create a command, resulting in a +compilation error. + +```java +// OK - requirements, body, and name are all provided +// Each builder method returns a different builder object that provides methods +// for progressing to the next stage. +Command command = Command.noRequirements().executing(...).named("Name"); + +// Compilation error! Missing the command body +Command command = Command.noRequirements().named("Name"); + +// Compilation error! Missing the command name +Command command = Command.noRequirements().executing(...); +``` + +#### Forced Naming + +Builder objects will only permit command creation when a name for the command has been provided. + +#### Automatic Naming + +Parallel and sequential groups and builders permit automatically generated names to be used instead +of manually specified ones. + +Automatic sequence names will simply be the list of all the commands in the sequence, separated by +a “->” arrow sign; for example, “Step 1 -> Step 2 -> Step 3” + +Automatic parallel group names will be the list of the names of the commands in the group, separated +by a pipe “|”. Required commands will be in their own group, separated from non-required commands. +If either subgroup is empty, then there will be no indication in the output (ie, no empty group +token like “()” will appear) + +### Cooperative Multitasking + +All commands *must* yield control back to the coroutine in their control loops. Because we cannot +preempt a running command at any arbitrary point, the command must explicitly and deliberately cede +control back to the scheduler in order for the framework to function. Commands may do this by +calling `coroutine.yield()` on the injected coroutine object, which ultimately delegates to +`Continuation.yield()` to pause the carrier continuation object. + +The scheduler has a single `ContinuationScope` object. When a command is mounted, the scheduler will +create a new `Continuation` object bound to that scope, and a `Coroutine` object bound to the +continuation. `ContinuationScope` and `Continuation` are currently JDK-private classes; the build +needs to open the module to the wpilib/unnamed module for reflective access at compile and runtime. +Because access is entirely reflective, wrapper classes at the library level will be used to make +access easier. + +### Scheduling + +Scheduling a command with `Scheduler.schedule()` will add the command to a queue of pending +commands. Any command in the queue that conflicts and has a lower priority will be removed from the +queue without calling the `onCancel` hook. At the start of `Scheduler.run()`, all commands that +conflict with those in the queue are cancelled, then the queued commands are promoted to running. + +Scheduling an inner command will bypass the queue and immediately begin to run. This avoids delays +caused by loop timings for deeply nested commands. + +### Scheduler `run()` Cycle + +1. Run periodic sideload functions +2. Poll the event loop for triggers (which may queue or cancel commands) +3. Schedule default commands for the next iteration to pick up and start running +4. Promote scheduled commands to running + 1. Cancels any running commands that conflict with scheduled ones +5. Iterate over running commands + 1. Mount + 2. Run until yield point is reached or an error is raised + 3. Unmount + 4. Evict if the command finished + 1. Any inner command still running is canceled + +### Interruption + +* Caused by scheduling a command that requires mechanisms already in use by one or more running + commands +* Results in cancellation of the conflicting commands + * Commands are moved to a pending-cancellation state + * Then when `run()` is next called, they are removed from the scheduler + * This occurs prior to the scheduled commands being promoted to the running state + * The conflicting command’s entire tree (all ancestors and children) will be cancelled +* Siblings in a composition can interrupt each other + * eg `fork(command1); fork(command2)`, where `command1` and `command2` share at least one + common requirement, would cause `command1` to be interrupted by `command2` + +### Telemetry + +Scheduler state is serialized using protobuf. The scheduler will send a list of the currently queued +commands and a list of the current running commands. Commands are serialized as (id: uint32, +parent_id: uint32, name: string, priority: int32, requirements: string array, +last_time_ms: double, total_time_ms: double). Consumers can use the `id` and `parent_id` attributes +to reconstruct the tree structure, if desired. `id` and `parent_id` marginally increase the size of +serialized data, but make the schema and deserialization quite simple. + +Command IDs are unique across all commands, including those that are not currently running. Every time +a command is scheduled, the scheduler will assign that run a unique ID. Note that this is stored as a +32-bit signed integer, so the maximum number of unique IDs is 2^31. No guarantee is made that IDs will +be unique across runs of the same program, nor to the order in which ID numbers are assigned; however, +IDs will be issued in a monotonically increasing fashion - a command scheduled before another will +always have a lower ID number. + +Records in the serialized output will be ordered by scheduling order. As a result, child commands +will always appear _after_ their parent. + +For example, if a scheduler is running a command like this: + +```java +Mechansism m1, m2; + +Command theCommand() { + return ParallelGroup.all( + m1.run(/* ... */).withPriority(1).named("Command 1"), + m2.run(/* ... */).withPriority(2).named("Command 2") + ).withAutomaticName(); +} +``` + +Telemetry for commands in the scheduler would look like: + +| `id` | `parent_id` | `name` | `priority` | `requirements` | `last_time_ms` | `total_time_ms` | +|--------|-------------|---------------------------|------------|----------------|----------------|-----------------| +| 347123 | -- | "(Command 1 & Command 2)" | 2 | ["M1", "M2"] | 0.210 | 5.122 | +| 998712 | 347123 | "Command 1" | 1 | ["M1"] | 0.051 | 1.241 | +| 591564 | 347123 | "Command 2" | 2 | ["M2"] | 0.108 | 3.249 | diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 3153b0234f..06f5acdcee 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -106,6 +106,7 @@ javadoc( deps = [ "//apriltag:apriltag-java", "//cameraserver:cameraserver-java", + "//commandsv3:commandsv3-java", "//cscore:cscore-java", "//epilogue-runtime:epilogue-java", "//hal:hal-java", diff --git a/docs/build.gradle b/docs/build.gradle index 65b758674f..489424de2b 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -5,6 +5,7 @@ plugins { evaluationDependsOn(':apriltag') evaluationDependsOn(':cameraserver') +evaluationDependsOn(':commandsv3') evaluationDependsOn(':cscore') evaluationDependsOn(':epilogue-runtime') evaluationDependsOn(':hal') @@ -161,6 +162,7 @@ task generateJavaDocs(type: Javadoc) { "-edu.wpi.first.math.system.plant.struct," + "-edu.wpi.first.math.trajectory.proto," + "-edu.wpi.first.math.trajectory.struct," + + "-org.wpilib.commands3.proto," + // The .measure package contains generated source files for which automatic javadoc // generation is very difficult to do meaningfully. "-edu.wpi.first.units.measure", true) @@ -170,6 +172,7 @@ task generateJavaDocs(type: Javadoc) { dependsOn project(':wpilibj').generateJavaVersion source project(':apriltag').sourceSets.main.java source project(':cameraserver').sourceSets.main.java + source project(':commandsv3').sourceSets.main.java source project(':cscore').sourceSets.main.java source project(':epilogue-runtime').sourceSets.main.java source project(':hal').sourceSets.main.java diff --git a/settings.gradle b/settings.gradle index efcb9caf55..f78ae8bd11 100644 --- a/settings.gradle +++ b/settings.gradle @@ -48,6 +48,7 @@ include 'simulation:halsim_xrp' include 'cameraserver' include 'cameraserver:multiCameraServer' include 'wpilibNewCommands' +include 'commandsv3' include 'romiVendordep' include 'xrpVendordep' include 'developerRobot' diff --git a/styleguide/checkstyle-suppressions.xml b/styleguide/checkstyle-suppressions.xml index 0ff41a1b98..8254e2b628 100644 --- a/styleguide/checkstyle-suppressions.xml +++ b/styleguide/checkstyle-suppressions.xml @@ -10,8 +10,9 @@ suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" checks="(LocalVariableName|MemberName|MethodName|MethodTypeParameterName|ParameterName)" /> - + + diff --git a/styleguide/pmd-ruleset.xml b/styleguide/pmd-ruleset.xml index 985db64620..c97ab738f4 100644 --- a/styleguide/pmd-ruleset.xml +++ b/styleguide/pmd-ruleset.xml @@ -15,6 +15,7 @@ .*/*IntegrationTests.* .*/*JNI.* .*/math/proto.* + .*/commands3/proto.* diff --git a/styleguide/spotbugs-exclude.xml b/styleguide/spotbugs-exclude.xml index bdebdc55f3..ac00f14803 100644 --- a/styleguide/spotbugs-exclude.xml +++ b/styleguide/spotbugs-exclude.xml @@ -205,6 +205,11 @@ + + + + + diff --git a/wpilib-config.cmake.in b/wpilib-config.cmake.in index 30080f24d9..1c3bb90ac6 100644 --- a/wpilib-config.cmake.in +++ b/wpilib-config.cmake.in @@ -8,6 +8,7 @@ find_dependency(Threads) @APRILTAG_DEP_REPLACE@ @CAMERASERVER_DEP_REPLACE@ @CSCORE_DEP_REPLACE@ +@COMMANDSV3_DEP_REPLACE@ @DATALOG_DEP_REPLACE@ @HAL_DEP_REPLACE@ @NTCORE_DEP_REPLACE@ diff --git a/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/SchedulerTest.java b/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/CommandSchedulerTest.java similarity index 99% rename from wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/SchedulerTest.java rename to wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/CommandSchedulerTest.java index 0a6eb3df5c..2d4a3e4ab9 100644 --- a/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/SchedulerTest.java +++ b/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/CommandSchedulerTest.java @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; -class SchedulerTest extends CommandTestBase { +class CommandSchedulerTest extends CommandTestBase { @Test void schedulerLambdaTestNoInterrupt() { try (CommandScheduler scheduler = new CommandScheduler()) {