From 21d921184a67a2f3dfc163a41118aa3e83333013 Mon Sep 17 00:00:00 2001 From: sciencewhiz Date: Tue, 22 Apr 2025 14:26:26 -0700 Subject: [PATCH] [examples] Add compilable code snippets (#7909) This enables frc-docs to use RLIs for things that are currently in-line code blocks, and ensures they compile, which is important with the 2027 breaking changes coming. They are kept separate from the examples to ensure they don't polute the VSCode examples finder. Adds the Encoder snippets used in the frc-docs Encoder article as the first instance of this. --- shared/examplecheck.gradle | 19 +++ wpilibcExamples/CMakeLists.txt | 14 ++ wpilibcExamples/build.gradle | 123 ++++++++++++++++-- .../main/cpp/snippets/Encoder/cpp/Robot.cpp | 69 ++++++++++ .../src/main/cpp/snippets/snippets.json | 12 ++ wpilibjExamples/build.gradle | 43 ++++++ .../first/wpilibj/snippets/encoder/Main.java | 25 ++++ .../first/wpilibj/snippets/encoder/Robot.java | 62 +++++++++ .../wpi/first/wpilibj/snippets/snippets.json | 13 ++ 9 files changed, 366 insertions(+), 14 deletions(-) create mode 100644 wpilibcExamples/src/main/cpp/snippets/Encoder/cpp/Robot.cpp create mode 100644 wpilibcExamples/src/main/cpp/snippets/snippets.json create mode 100644 wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Main.java create mode 100644 wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Robot.java create mode 100644 wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/snippets.json diff --git a/shared/examplecheck.gradle b/shared/examplecheck.gradle index 644c095cf2..27d90762de 100644 --- a/shared/examplecheck.gradle +++ b/shared/examplecheck.gradle @@ -103,6 +103,24 @@ task checkExamples(type: Task) { } } +task checkSnippets(type: Task) { + doLast { + def parsedJson = new groovy.json.JsonSlurper().parseText(snippetsFile.text) + fileCheck(parsedJson, snippetsDirectory) + parsedJson.each { + assert it.name != null + assert it.description != null + assert it.tags != null + assert it.tags.findAll { !tagList.contains(it) }.empty + assert it.foldername != null + assert it.gradlebase != null + if (it.gradlebase == 'java') { + assert it.mainclass != null + } + } + } +} + task checkCommands(type: Task) { doLast { def parsedJson = new groovy.json.JsonSlurper().parseText(commandFile.text) @@ -127,3 +145,4 @@ task checkCommands(type: Task) { check.dependsOn checkTemplates check.dependsOn checkExamples check.dependsOn checkCommands +check.dependsOn checkSnippets diff --git a/wpilibcExamples/CMakeLists.txt b/wpilibcExamples/CMakeLists.txt index aaec73c88f..f79cd15d68 100644 --- a/wpilibcExamples/CMakeLists.txt +++ b/wpilibcExamples/CMakeLists.txt @@ -5,6 +5,7 @@ include(SubDirList) subdir_list(TEMPLATES ${CMAKE_SOURCE_DIR}/wpilibcExamples/src/main/cpp/templates) subdir_list(EXAMPLES ${CMAKE_SOURCE_DIR}/wpilibcExamples/src/main/cpp/examples) +subdir_list(SNIPPETS ${CMAKE_SOURCE_DIR}/wpilibcExamples/src/main/cpp/snippets) add_custom_target(wpilibcExamples) add_custom_target(wpilibcExamples_test) @@ -63,3 +64,16 @@ foreach(template ${TEMPLATES}) target_link_libraries(${template} wpilibc wpilibNewCommands romiVendordep xrpVendordep) add_dependencies(wpilibcExamples ${template}) endforeach() + +foreach(snippet ${SNIPPETS}) + file( + GLOB_RECURSE sources + src/main/cpp/snippets/${snippet}/cpp/*.cpp + src/main/cpp/snippets/${snippet}/c/*.c + ) + add_executable(snippet${snippet} ${sources}) + wpilib_target_warnings(${snippet}) + target_include_directories(snippet${snippet} PUBLIC src/main/cpp/snippets/${snippet}/include) + target_link_libraries(snippet${snippet} wpilibc wpilibNewCommands romiVendordep xrpVendordep) + add_dependencies(wpilibcExamples snippet${snippet}) +endforeach() diff --git a/wpilibcExamples/build.gradle b/wpilibcExamples/build.gradle index eb1aca5251..aa0aaedef4 100644 --- a/wpilibcExamples/build.gradle +++ b/wpilibcExamples/build.gradle @@ -14,9 +14,20 @@ apply from: "${rootDir}/shared/googletest.gradle" ext.examplesMap = [:] ext.templatesMap = [:] +ext.snippetsMap = [:] -File examplesTree = file("$projectDir/src/main/cpp/examples") -examplesTree.list(new FilenameFilter() { +ext { + templateDirectory = new File("$projectDir/src/main/cpp/templates/") + templateFile = new File("$projectDir/src/main/cpp/templates/templates.json") + exampleDirectory = new File("$projectDir/src/main/cpp/examples/") + exampleFile = new File("$projectDir/src/main/cpp/examples/examples.json") + commandDirectory = new File("$projectDir/src/main/cpp/commands/") + commandFile = new File("$projectDir/src/main/cpp/commands/commands.json") + snippetsDirectory = new File("$projectDir/src/main/cpp/snippets/") + snippetsFile = new File("$projectDir/src/main/cpp/snippets/snippets.json") +} + +exampleDirectory.list(new FilenameFilter() { @Override public boolean accept(File current, String name) { return new File(current, name).isDirectory(); @@ -24,8 +35,7 @@ examplesTree.list(new FilenameFilter() { }).each { examplesMap.put(it, []) } -File templatesTree = file("$projectDir/src/main/cpp/templates") -templatesTree.list(new FilenameFilter() { +templateDirectory.list(new FilenameFilter() { @Override public boolean accept(File current, String name) { return new File(current, name).isDirectory(); @@ -33,6 +43,14 @@ templatesTree.list(new FilenameFilter() { }).each { templatesMap.put(it, []) } +snippetsDirectory.list(new FilenameFilter() { + @Override + public boolean accept(File current, String name) { + return new File(current, name).isDirectory(); + } + }).each { + snippetsMap.put(it, []) + } nativeUtils.platformConfigs.named(nativeUtils.wpi.platforms.windowsx64).configure { linker.args.remove('/DEBUG:FULL') @@ -41,7 +59,7 @@ nativeUtils.platformConfigs.named(nativeUtils.wpi.platforms.windowsx64).configur } ext { - sharedCvConfigs = examplesMap + templatesMap + [commands: []] + sharedCvConfigs = examplesMap + templatesMap + snippetsMap.collectEntries { key, value -> ['snippets' + key, value] } + [commands: []] staticCvConfigs = [:] useJava = false useCpp = true @@ -171,6 +189,57 @@ model { } } } + snippetsMap.each { key, value -> + "snippets${key}"(NativeExecutableSpec) { + targetBuildTypes 'debug' + binaries.all { binary -> + lib project: ':wpilibNewCommands', library: 'wpilibNewCommands', linkage: 'shared' + lib project: ':romiVendordep', library: 'romiVendordep', linkage: 'shared' + lib project: ':xrpVendordep', library: 'xrpVendordep', linkage: 'shared' + lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' + lib project: ':apriltag', library: 'apriltag', linkage: 'shared' + lib project: ':wpimath', library: 'wpimath', linkage: 'shared' + project(':ntcore').addNtcoreDependency(binary, 'shared') + lib project: ':cscore', library: 'cscore', linkage: 'shared' + project(':hal').addHalDependency(binary, 'shared') + lib project: ':cameraserver', library: 'cameraserver', linkage: 'shared' + lib project: ':wpinet', library: 'wpinet', linkage: 'shared' + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + if (binary.targetPlatform.name == nativeUtils.wpi.platforms.roborio) { + nativeUtils.useRequiredLibrary(binary, 'ni_link_libraries', 'ni_runtime_libraries') + } + if (binary.targetPlatform.name == getCurrentArch()) { + simModules.each { + lib project: ":simulation:$it", library: it, linkage: 'shared' + } + } + } + sources { + cpp { + source { + srcDirs 'src/main/cpp/snippets/' + "${key}" + "/cpp" + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/main/cpp/snippets/' + "${key}" + "/include" + include '**/*.h' + } + } + } + sources { + c { + source { + srcDirs 'src/main/cpp/snippets/' + "${key}" + "/c" + include '**/*.c' + } + exportedHeaders { + srcDirs 'src/main/cpp/snippets/' + "${key}" + "/include" + include '**/*.h' + } + } + } + } + } } testSuites { examplesMap.each { key, value -> @@ -207,6 +276,41 @@ model { } } } + testSuites { + snippetsMap.each { key, value -> + def testFolder = new File("${rootDir}/wpilibcExamples/src/test/cpp/snippets/${key}") + if (testFolder.exists()) { + "snippets${key}Test"(GoogleTestTestSuiteSpec) { + for (NativeComponentSpec c : $.components) { + if (c.name == key) { + testing c + break + } + } + sources { + cpp { + source { + srcDirs "src/test/cpp/examples/${key}/cpp" + include '**/*.cpp' + } + exportedHeaders { + srcDirs "src/test/cpp/examples/${key}/include" + } + } + c { + source { + srcDirs "src/test/cpp/examples/${key}/c" + include '**/*.c' + } + exportedHeaders { + srcDirs "src/test/cpp/examples/${key}/include" + } + } + } + } + } + } + } binaries { withType(GoogleTestTestSuiteBinarySpec) { lib project: ':wpilibNewCommands', library: 'wpilibNewCommands', linkage: 'shared' @@ -253,15 +357,6 @@ model { } } -ext { - templateDirectory = new File("$projectDir/src/main/cpp/templates/") - templateFile = new File("$projectDir/src/main/cpp/templates/templates.json") - exampleDirectory = new File("$projectDir/src/main/cpp/examples/") - exampleFile = new File("$projectDir/src/main/cpp/examples/examples.json") - commandDirectory = new File("$projectDir/src/main/cpp/commands/") - commandFile = new File("$projectDir/src/main/cpp/commands/commands.json") -} - model { // Create run tasks for all examples. tasks { diff --git a/wpilibcExamples/src/main/cpp/snippets/Encoder/cpp/Robot.cpp b/wpilibcExamples/src/main/cpp/snippets/Encoder/cpp/Robot.cpp new file mode 100644 index 0000000000..7f736d5c1b --- /dev/null +++ b/wpilibcExamples/src/main/cpp/snippets/Encoder/cpp/Robot.cpp @@ -0,0 +1,69 @@ +// 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. + +#include + +#include +#include +#include +#include + +/** + * Encoder snippets for frc-docs. + * https://docs.wpilib.org/en/stable/docs/software/hardware-apis/sensors/encoders-software.html + */ +WPI_IGNORE_DEPRECATED +class Robot : public frc::TimedRobot { + public: + Robot() { + // Configures the encoder to return a distance of 4 for every 256 pulses + // Also changes the units of getRate + m_encoder.SetDistancePerPulse(4.0 / 256.0); + // Configures the encoder to consider itself stopped after .1 seconds + m_encoder.SetMaxPeriod(0.1_s); + // Configures the encoder to consider itself stopped when its rate is below + // 10 + m_encoder.SetMinRate(10); + // Reverses the direction of the encoder + m_encoder.SetReverseDirection(true); + // Configures an encoder to average its period measurement over 5 samples + // Can be between 1 and 127 samples + m_encoder.SetSamplesToAverage(5); + } + + void TeleopPeriodic() override { + // Gets the distance traveled + m_encoder.GetDistance(); + + // Gets the current rate of the encoder + m_encoder.GetRate(); + + // Gets whether the encoder is stopped + m_encoder.GetStopped(); + + // Gets the last direction in which the encoder moved + m_encoder.GetDirection(); + + // Gets the current period of the encoder + m_encoder.GetPeriod(); + + // Resets the encoder to read a distance of zero + m_encoder.Reset(); + } + + private: + // Initializes an encoder on DIO pins 0 and 1 + // Defaults to 4X decoding and non-inverted + frc::Encoder m_encoder{0, 1}; + + // Initializes an encoder on DIO pins 0 and 1 + // 2X encoding and non-inverted + frc::Encoder m_encoder2x{0, 1, false, frc::Encoder::EncodingType::k2X}; +}; + +#ifndef RUNNING_FRC_TESTS +int main() { + return frc::StartRobot(); +} +#endif diff --git a/wpilibcExamples/src/main/cpp/snippets/snippets.json b/wpilibcExamples/src/main/cpp/snippets/snippets.json new file mode 100644 index 0000000000..884e075dfd --- /dev/null +++ b/wpilibcExamples/src/main/cpp/snippets/snippets.json @@ -0,0 +1,12 @@ +[ + { + "name": "Encoder", + "description": "Snippets of Encoder class usage for frc-docs.", + "tags": [ + "Hardware", + "Encoder" + ], + "foldername": "Encoder", + "gradlebase": "cpp" + } +] diff --git a/wpilibjExamples/build.gradle b/wpilibjExamples/build.gradle index 115b253965..eec38c0a35 100644 --- a/wpilibjExamples/build.gradle +++ b/wpilibjExamples/build.gradle @@ -72,6 +72,8 @@ ext { exampleFile = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/examples/examples.json") commandDirectory = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/commands/") commandFile = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/commands/commands.json") + snippetsDirectory = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/snippets/") + snippetsFile = new File("$projectDir/src/main/java/edu/wpi/first/wpilibj/snippets/snippets.json") } apply plugin: 'cpp' @@ -203,6 +205,47 @@ model { test.dependsOn(testTask) } } + new groovy.json.JsonSlurper().parseText(snippetsFile.text).each { entry -> + project.tasks.create("runSnippet${entry.foldername}", JavaExec) { run -> + run.group "run snippets" + run.mainClass = "edu.wpi.first.wpilibj.snippets." + entry.foldername + "." + entry.mainclass + run.classpath = sourceSets.main.runtimeClasspath + run.dependsOn it.tasks.install + run.systemProperty 'java.library.path', filePath + doFirst { doFirstTask(run) } + + if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { + run.jvmArgs = ['-XstartOnFirstThread'] + } + } + project.tasks.create("testSnippet${entry.foldername}", Test) { testTask -> + testTask.group "verification" + testTask.useJUnitPlatform() + testTask.filter { + includeTestsMatching("edu.wpi.first.wpilibj.snippets.${entry.foldername}.*") + setFailOnNoMatchingTests(false) + } + test.filter { + excludeTestsMatching("edu.wpi.first.wpilibj.snippets.${entry.foldername}.*") + setFailOnNoMatchingTests(false) + } + testTask.testClassesDirs = sourceSets.test.output.classesDirs + testTask.classpath = sourceSets.test.runtimeClasspath + testTask.dependsOn it.tasks.install + + testTask.systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' + testTask.testLogging { + events "failed" + exceptionFormat "full" + } + testTask.systemProperty 'java.library.path', filePath + + if (project.hasProperty('onlylinuxathena') || project.hasProperty('onlylinuxarm32') || project.hasProperty('onlylinuxarm64') || project.hasProperty('onlywindowsarm64')) { + testTask.enabled = false + } + test.dependsOn(testTask) + } + } found = true } diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Main.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Main.java new file mode 100644 index 0000000000..4fef5feb29 --- /dev/null +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Main.java @@ -0,0 +1,25 @@ +// 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 edu.wpi.first.wpilibj.snippets.encoder; + +import edu.wpi.first.wpilibj.RobotBase; + +/** + * Do NOT add any static variables to this class, or any initialization at all. Unless you know what + * you are doing, do not modify this file except to change the parameter class to the startRobot + * call. + */ +public final class Main { + private Main() {} + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Robot.java new file mode 100644 index 0000000000..de1511bd55 --- /dev/null +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/encoder/Robot.java @@ -0,0 +1,62 @@ +// 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 edu.wpi.first.wpilibj.snippets.encoder; + +import edu.wpi.first.wpilibj.Encoder; +import edu.wpi.first.wpilibj.TimedRobot; + +/** + * Encoder snippets for frc-docs. + * https://docs.wpilib.org/en/stable/docs/software/hardware-apis/sensors/encoders-software.html + */ +@SuppressWarnings("deprecation") +public class Robot extends TimedRobot { + // Initializes an encoder on DIO pins 0 and 1 + // Defaults to 4X decoding and non-inverted + Encoder m_encoder = new Encoder(0, 1); + + // Initializes an encoder on DIO pins 0 and 1 + // 2X encoding and non-inverted + Encoder m_encoder2x = new Encoder(0, 1, false, Encoder.EncodingType.k2X); + + /** Called once at the beginning of the robot program. */ + public Robot() { + // Configures the encoder to return a distance of 4 for every 256 pulses + // Also changes the units of getRate + m_encoder.setDistancePerPulse(4.0 / 256.0); + // Configures the encoder to consider itself stopped after .1 seconds + m_encoder.setMaxPeriod(0.1); + // Configures the encoder to consider itself stopped when its rate is below 10 + m_encoder.setMinRate(10); + // Reverses the direction of the encoder + m_encoder.setReverseDirection(true); + // Configures an encoder to average its period measurement over 5 samples + // Can be between 1 and 127 samples + m_encoder.setSamplesToAverage(5); + + m_encoder2x.getRate(); + } + + @Override + public void teleopPeriodic() { + // Gets the distance traveled + m_encoder.getDistance(); + + // Gets the current rate of the encoder + m_encoder.getRate(); + + // Gets whether the encoder is stopped + m_encoder.getStopped(); + + // Gets the last direction in which the encoder moved + m_encoder.getDirection(); + + // Gets the current period of the encoder + m_encoder.getPeriod(); + + // Resets the encoder to read a distance of zero + m_encoder.reset(); + } +} diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/snippets.json b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/snippets.json new file mode 100644 index 0000000000..8e8786f713 --- /dev/null +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/snippets/snippets.json @@ -0,0 +1,13 @@ +[ + { + "name": "Encoder", + "description": "Snippets of Encoder class usage for frc-docs.", + "tags": [ + "Hardware", + "Encoder" + ], + "foldername": "encoder", + "gradlebase": "java", + "mainclass": "Main" + } +]