Compare commits

...

53 Commits

Author SHA1 Message Date
Michael Lesirge
2b43541b94 [wpimath] MathUtil: Add 2D variants of applyDeadband and copySignPow (#8057) 2025-10-10 13:44:12 -07:00
arbessette
4c4996e638 [docs] Remove Private Message language (#8202)
Removing private written message for safety of all users and contributors.
2025-10-10 12:43:02 -07:00
Sam Carlberg
cfbd7a5af2 [build] Fix doxygen builds on apple CPUs (#8282)
Caused by the doxygen gradle plugin attempting to download 1.10.0 (presumably its default version) from artifactory because the 1.12.0 config is only applied on x86_64 platforms. Just fixing that isn't enough, however; on mac, the plugin would fail to extract the dmg. We need to fall back to a global installation on the PATH for the plugin to find, preferentially using that instead of a failed attempt to download and extract the dmg.
2025-10-10 12:42:02 -07:00
Tyler Veness
f5990e8b40 [upstream_utils] Fix Eigen tag (#8283)
Upstream no longer seems to have the commit we were pointing to. We'll
just use the tag since that hasn't changed since the official release
announcement.
2025-10-09 21:50:55 -07:00
sciencewhiz
b56b843c8a Update frcYear in vendordeps (#8276) 2025-10-07 22:00:04 -07:00
sciencewhiz
2fb5271cc9 [build] Update native-utils to 2026 (#8277) 2025-10-05 14:22:53 -07:00
Peter Lilley
f1b9be551b [wpiutil] Add reverse/bidirectional iterators to wpi::circular_buffer (#8275)
Use std::reverse_iterator<> to create reverse iterators, make other
iterators bidirectional to allow for this. Added unit tests.
2025-10-03 23:13:55 -07:00
Sam Carlberg
3972b01c51 Add javac plugin for detecting common error cases at compile time (#8196)
Adds a `@NoDiscard` annotation that can be placed on methods to guarantee their return values are used and on types to guarantee that any method returning that type uses the return value.

Methods that call `@NoDiscard`-annotated functions can add a `@SuppressWarnings("NoDiscard")` or `@SuppressWarnings("all")` annotation (or annotation on the class declaring that method) to silence the compiler error warnings.
2025-10-03 17:42:47 -07:00
Joseph Eng
871769c815 [wpimath] Fix units overload resolution (#8267) 2025-10-02 17:36:30 -07:00
Peter Johnson
ca7718cb08 [glass] FMS: Fix reading past end of GSM buffer (#8268) 2025-10-02 17:35:50 -07:00
Joseph Eng
5e7e5306df [wpiutil] Update StructSerializable contract (NFC) (#7441)
Matches ProtobufSerializable.  This is necessary for generic types.
2025-09-30 14:57:42 -06:00
Gold856
6447011bc3 [ci] Consolidate docs jobs (#7910)
We build docs in three different places, which is annoying to deal with, and it means we build docs two more times than necessary. Now, docs are built just once in the main Gradle workflow, with warnings promoted to errors, eliminating the need for the separate job in lint-format.yml. The uploaded docs artifact is then unpacked and commited to the GitHub Pages repo like normal.
2025-09-29 18:02:42 -07:00
Tyler Veness
bb5ee73e46 [build] Build JNI and benchmarks as release by default (#8257)
It doesn't make sense to benchmark debug binaries. Also, wpimath JNI
performance in unit tests is drastically impacted by debug vs release.
2025-09-29 17:47:17 -07:00
Gold856
0277759d44 [wpiunits] Remove redundant if statement and inaccurate comment (#8262) 2025-09-28 16:00:35 -07:00
Gold856
f9899eb73f [wpimath] Fix odd header path in BangBangController (#8261) 2025-09-27 22:48:00 -07:00
Tyler Veness
6b8be313c7 [build] Fix Java 25 builds (#8245)
I'm able to use a local install of Gradle 9.1 that has Java 25 support,
but some plugin upgrades are needed as well.
2025-09-25 21:28:37 -07:00
Tyler Veness
ab53d51c6f Fix or suppress clang-tidy warnings (#8254) 2025-09-25 21:28:04 -07:00
Tyler Veness
5003939b64 [upstream_utils] Recopy Eigen source (#8251)
Upstream slid the tag (again).  Change to the SHA commit ID until things stabilize.
2025-09-24 09:03:16 -06:00
Tyler Veness
ab259c2e89 [build] Fix Gradle 9 archives deprecation warning (#8247)
The deprecation message was:
```
The `archives` configuration added by the `base` plugin has been
deprecated and will be removed in Gradle 10.0.0. Adding artifacts to the
`archives` configuration will now result in a deprecation warning. If
you want the artifact built when running the `assemble` task, you should
add the artifact (or the task that produces it) as a dependency of the
`assemble` task directly.

val specialJar = tasks.register<Jar>("specialJar") {
    archiveBaseName.set("special")
    from("build/special")
}
tasks.named("assemble") {
    dependsOn(specialJar)
}
```
2025-09-22 11:58:14 -06:00
Tyler Veness
8fb5a1985a [upstream_utils] Recopy Eigen source (#8249)
Upstream slid the tag.
2025-09-21 21:58:43 -07:00
sciencewhiz
1e50471d2c [build] Update gradle-jni to 1.2.0 for Gradle 9 support (#8246) 2025-09-21 08:14:37 -07:00
sciencewhiz
c575a23e8e [build] Fix wpical and sysid icons on macOS (#8243)
Fixes #8239
2025-09-20 20:30:44 -07:00
sciencewhiz
4522cca70f [build] Update to develocity plugin (#8242)
Gradle enterprise plugin has been replaced by develocity.
2025-09-20 20:30:05 -07:00
sciencewhiz
850a148aad [build] Fix gradle 9 deprecations in msvc runtime (#8244) 2025-09-20 17:59:29 -07:00
Tyler Veness
0a4e44ea06 [wpimath] Synchronize C++ and Java RK4 docs (NFC) (#8238) 2025-09-20 15:49:41 -07:00
Tyler Veness
a7e7f6912a [upstream_utils] Upgrade to Eigen 5.0.0 (#8240) 2025-09-20 15:49:23 -07:00
Sam Carlberg
ee0a8a1e56 [epilogue] Support logging of protobuf-serializable types (#8229)
For parity with struct-serializable types.

Change struct serialization to only apply to types with a public static final <type> struct field, instead of relying only on the marker interface (which is not always followed). Doing this allows fallthrough to the protobuf handler for types with dynamic structs but static protobuf serializers.
2025-09-20 11:23:22 -07:00
Tyler Veness
3dbdfa1839 [upstream_utils] Upgrade Sleipnir (#8235) 2025-09-20 11:21:06 -07:00
Tyler Veness
ee3d55e848 [upstream_utils] Upgrade Eigen to latest (#8228) 2025-09-19 17:52:48 -06:00
Tyler Veness
dbffe6e8ac [wpimath] Add readme (#8209) 2025-09-08 17:26:22 -07:00
Ryan Blue
2639e0365b [ci] Update sentinel build with Windows FFI changes (#8218) 2025-09-07 21:33:19 -07:00
Ryan Blue
08f11488b0 [ci] Add 2027 development repo to cleanup task (#8217) 2025-09-07 06:16:26 -07:00
Tyler Veness
632749e6f3 [build] Upgrade Maven dependencies (#8173) 2025-09-01 08:13:46 -07:00
Jade
b0829356fa [wpimath] Fix sysid links (NFC) (#8204)
Signed-off-by: Jade Turner <spacey-sooty@proton.me>
2025-09-01 08:11:39 -07:00
Ryan Blue
ed904851eb [ci] Fix CMake Android build caching (#8206)
sccache was enabled but didn't have write credentials
2025-08-31 21:55:13 -07:00
Wispy
2cfd58f119 [commands] Add Subsystem.idle() (#7815) 2025-08-30 22:54:53 -07:00
Sam Carlberg
129cbbe11d [epilogue] Optimize time and memory usage of epilogue backends (#8190) 2025-08-30 20:15:22 -07:00
Ryan Shavell
45db0fd45e [epilogue] Use reflection to access non-public superclass fields (#7996)
Co-authored-by: Sam Carlberg <sam@slfc.dev>
2025-08-30 20:14:41 -07:00
Kevin-OConnor
9fd4ccf95b [hal] Add CAN Mfgrs and Adjust Device Types (#8201) 2025-08-30 11:36:26 -07:00
sciencewhiz
4e6b9706ff [build] Explicitly set Documentation archiveVersion (#8192)
In the recent gradle or gradle dependencies update, the documentation
zips were being published as documentation-version-unspecified, where
the unspecified was coming from archiveVersion. It looks like we use a
different method of setting the version, so make sure that
archiveVersion is empty
2025-08-26 08:09:18 -06:00
Tyler Veness
0d9e850e22 [wpimath] Fix dt type in C++ tests (#8179)
The UKF test was calling `.value()` on an implicit
`units::millisecond_t` type assuming it was `units::second_t`.

I normalized the rest of the dt declarations while I was at it.
2025-08-16 22:51:13 -07:00
sciencewhiz
46a3318324 [build] Fix processstarter publishing (#8171)
Artifacts weren't in OS and architecture subfolders in the zip like all the other
C++ tools
2025-08-11 20:52:10 -07:00
Daniel Chen
f209ecb0cb [wpimath] Add structs for TrapezoidProfile.State and ExponentialProfile.State (#8163) 2025-08-10 11:45:36 -07:00
Iris
78fa67099e [build] Small fixes to build on GCC 15 (#8148)
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-08-09 00:07:41 -07:00
Tyler Veness
9ac7e286f5 [build] Upgrade Gradle plugins (#8166)
I upgraded all plugins I could see except org.ysb33r.doxygen. 2.0 made
breaking changes, and I couldn't figure out how to migrate.

Most of the changes are for suppressing new linter purification rites.
2025-08-08 23:04:02 -07:00
Tyler Veness
5fd9e1e72a [build] Fix Gradle Task.project deprecation warning (#8167)
```
> Task :wpilibcExamples:checkCommands
Script '/home/tav/frc/wpilib/allwpilib/shared/examplecheck.gradle': line 135
Invocation of Task.project at execution time has been deprecated. This will fail with an error in Gradle 10.0. This API is incompatible with the configuration cache, which will become the only mode supported by Gradle in a future release. Consult the upgrading guide for further information: https://docs.gradle.org/8.14.3/userguide/upgrading_version_7.html#task_project
        at examplecheck_4wsg1s37eigy9vs5arzst20ga$_run_closure5$_closure16$_closure17.doCall$original(/home/tav/frc/wpilib/allwpilib/shared/examplecheck.gradle:135)
        (Run with --stacktrace to get the full stack trace of this deprecation warning.)
```

Moving the project access outside the doLast block makes it occur at
confguration time instead.
2025-08-08 22:48:11 -07:00
Tyler Veness
0a0adebd89 [build] Upgrade to Gradle 8.14.3 (#8164)
This fixes local builds with JDK 24.

I fixed deprecation warnings from `./gradlew wrapper --warning-mode all`
as well.
2025-08-08 09:08:34 -06:00
Gold856
2d11946d98 [wpical] Use updated thirdparty-ceres and move resource files (#8151) 2025-08-03 11:41:25 -07:00
Gold856
c42fde5d07 [ci] Make upstream_utils check error with a more clear message (#8153)
Now it links to the README in upstream_utils.
2025-08-03 11:37:40 -07:00
Rain Heuer
b3aeee18c8 [wpimath] Add vector product and squared length operations to Translation2d/3d (#8133)
Adds methods to compute the dot and cross products between Translation2ds and Translation3ds, as well as methods to compute the square of Distance and Norm, which allows avoiding some calls to sqrt in many cases.

Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-07-31 21:05:39 -07:00
Tyler Veness
feee88f40d [wpimath] Remove redundant transposes on symmetric matrices (#8131)
This likely won't have a performance impact since it only affects matrix traversal order, but it does simplify the code.
2025-07-31 21:04:55 -07:00
Peter Johnson
0478176e47 [simgui] Add GUI context getter hooks (#8127)
This enables GUI libraries to be linked statically with shared context.
2025-07-30 21:29:24 -07:00
Tyler Veness
e678a338b4 [ci] Upgrade wpiformat (#8124)
See https://github.com/wpilibsuite/styleguide/pull/312
2025-07-30 11:10:12 -06:00
462 changed files with 7966 additions and 3085 deletions

View File

@@ -45,6 +45,8 @@ Checks:
-clang-diagnostic-#warnings,
-clang-diagnostic-pedantic,
clang-analyzer-*,
-clang-analyzer-optin.cplusplus.UninitializedObject,
-clang-analyzer-security.FloatLoopCounter,
cppcoreguidelines-slicing,
google-build-namespaces,
google-explicit-constructor,

View File

@@ -1,6 +1,4 @@
color: false
definitions: [cmake/modules]
line_length: 100
list_expansion: favour-inlining
quiet: false
unsafe: false

View File

@@ -3,7 +3,10 @@
{
"aql": {
"items.find": {
"repo": "wpilib-mvn-development-local",
"$or":[
{ "repo": "wpilib-mvn-development-local" },
{ "repo": "wpilib-mvn-development-2027-local" }
],
"path": { "$nmatch":"*edu/wpi/first/thirdparty*" },
"$or":[
{

View File

@@ -46,6 +46,12 @@ jobs:
- name: configure
run: cmake --preset with-sccache -DCMAKE_BUILD_TYPE=RelWithDebInfo -DWITH_WPILIB=OFF -DWITH_GUI=OFF -DWITH_CSCORE=OFF -DWITH_TESTS=OFF -DWITH_SIMULATION_MODULES=OFF -DWITH_PROTOBUF=OFF -DWITH_JAVA=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_TOOLCHAIN_FILE=${{ steps.setup-ndk.outputs.ndk-path }}/build/cmake/android.toolchain.cmake -DANDROID_ABI="${{ matrix.abi }}" -DANDROID_PLATFORM=android-24
env:
SCCACHE_WEBDAV_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
SCCACHE_WEBDAV_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
- name: build
run: cmake --build build-cmake --parallel $(nproc)
env:
SCCACHE_WEBDAV_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
SCCACHE_WEBDAV_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}

View File

@@ -1,67 +0,0 @@
name: Documentation
on: [push, workflow_dispatch]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
BASE_PATH: allwpilib/docs
jobs:
publish:
name: "Documentation - Publish"
runs-on: ubuntu-22.04
if: github.repository == 'wpilibsuite/allwpilib' && (github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '2027')))
concurrency: ci-docs-publish
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: gradle/actions/wrapper-validation@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
- name: Set environment variables (Development)
run: |
echo "BRANCH=development" >> $GITHUB_ENV
if: github.ref == 'refs/heads/main'
- name: Set environment variables (Tag)
run: |
echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
echo "BRANCH=beta" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '2027')
- name: Set environment variables (Release)
run: |
echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
echo "BRANCH=release" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') && !contains(github.ref, '2027')
- name: Build with Gradle
run: ./gradlew docs:generateJavaDocs docs:doxygen -PbuildServer ${{ env.EXTRA_GRADLE_ARGS }}
- name: Install SSH Client 🔑
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GH_DEPLOY_KEY }}
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.6.1
with:
ssh-key: true
repository-name: wpilibsuite/wpilibsuite.github.io
branch: allwpilib-${{ env.BRANCH }}
clean: true
single-commit: true
folder: docs/build/docs
- name: Trigger Workflow
uses: actions/github-script@v7
with:
github-token: ${{ secrets.DISPATCH_PAT_TOKEN }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: 'wpilibsuite.github.io',
workflow_id: 'static.yml',
ref: 'main',
})

View File

@@ -31,6 +31,9 @@ def main():
# Replace GCC warning argument with one Clang recognizes
elif arg == "-Wno-maybe-uninitialized":
out_args.append("-Wno-uninitialized")
# Skip GCC-specific warning argument
elif arg == "-Wno-error=restrict":
pass
else:
out_args.append(arg)

View File

@@ -203,7 +203,7 @@ jobs:
run: echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '2027')
- name: Build with Gradle
run: ./gradlew docs:zipDocs --build-cache -PbuildServer ${{ env.EXTRA_GRADLE_ARGS }}
run: ./gradlew docs:zipDocs --build-cache -PbuildServer -PdocWarningsAsErrors ${{ env.EXTRA_GRADLE_ARGS }}
env:
ARTIFACTORY_PUBLISH_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PUBLISH_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
@@ -212,6 +212,68 @@ jobs:
name: Documentation
path: docs/build/outputs
publish:
name: "Documentation - Publish"
runs-on: ubuntu-22.04
if: github.repository == 'wpilibsuite/allwpilib' && (github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '2027')))
needs: [build-documentation]
concurrency: ci-docs-publish
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Download docs artifacts
uses: actions/download-artifact@v4
with:
name: Documentation
- name: Make output directories
run: |
mkdir -p docs/tmp/doxygen/html
mkdir -p docs/tmp/javadoc
- name: Extract docs
run: |
unzip _GROUP_edu_wpi_first_wpilibc_ID_documentation_CLS.zip -d docs/tmp/doxygen/html
unzip _GROUP_edu_wpi_first_wpilibj_ID_documentation_CLS.zip -d docs/tmp/javadoc
- name: Set environment variables (Development)
run: |
echo "BRANCH=development" >> $GITHUB_ENV
if: github.ref == 'refs/heads/main'
- name: Set environment variables (Tag)
run: |
echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
echo "BRANCH=beta" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '2027')
- name: Set environment variables (Release)
run: |
echo "EXTRA_GRADLE_ARGS=-PreleaseMode" >> $GITHUB_ENV
echo "BRANCH=release" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') && !contains(github.ref, '2027')
- name: Install SSH Client 🔑
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GH_DEPLOY_KEY }}
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.6.1
with:
ssh-key: true
repository-name: wpilibsuite/wpilibsuite.github.io
branch: allwpilib-${{ env.BRANCH }}
clean: true
single-commit: true
folder: docs/tmp
- name: Trigger Workflow
uses: actions/github-script@v7
with:
github-token: ${{ secrets.DISPATCH_PAT_TOKEN }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: 'wpilibsuite.github.io',
workflow_id: 'static.yml',
ref: 'main',
})
combine:
name: Combine
needs: [build-docker, build-host, build-documentation]

View File

@@ -36,7 +36,7 @@ jobs:
- name: Install wpiformat
run: |
python -m venv ${{ runner.temp }}/wpiformat
${{ runner.temp }}/wpiformat/bin/pip3 install wpiformat==2025.33
${{ runner.temp }}/wpiformat/bin/pip3 install wpiformat==2025.34
- name: Run
run: ${{ runner.temp }}/wpiformat/bin/wpiformat
- name: Check output
@@ -78,7 +78,7 @@ jobs:
- name: Install wpiformat
run: |
python -m venv ${{ runner.temp }}/wpiformat
${{ runner.temp }}/wpiformat/bin/pip3 install wpiformat==2025.33
${{ runner.temp }}/wpiformat/bin/pip3 install wpiformat==2025.34
- name: Create compile_commands.json
run: |
./gradlew generateCompileCommands -Ptoolchain-optional-roboRio
@@ -123,18 +123,3 @@ jobs:
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if: ${{ failure() }}
documentation:
name: "Documentation"
runs-on: ubuntu-22.04
needs: [validation]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- name: Build with Gradle
run: ./gradlew docs:zipDocs -PbuildServer -PdocWarningsAsErrors ${{ env.EXTRA_GRADLE_ARGS }}

View File

@@ -103,9 +103,16 @@ jobs:
task: "build"
outputs: "build/allOutputs"
- os: windows-2022
artifact-name: Win32
artifact-name: Win32FFI
architecture: x86
task: ":ntcoreffi:build"
build-options: "-Pntcoreffibuild \"-Dorg.gradle.jvmargs=-Xmx1096m\""
outputs: "ntcoreffi/build/outputs"
- os: windows-2022
artifact-name: Win64FFI
architecture: x64
task: ":ntcoreffi:build"
build-options: "-Pntcoreffibuild -Pbuildwinarm64"
outputs: "ntcoreffi/build/outputs"
name: "Build - ${{ matrix.artifact-name }}"
runs-on: ${{ matrix.os }}

View File

@@ -141,4 +141,11 @@ jobs:
- name: Add untracked files to index so they count as changes
run: git add -A
- name: Check output
run: git --no-pager diff --exit-code HEAD ':!*.bazel'
run: |
set +e
git --no-pager diff --exit-code HEAD ':!*.bazel'
git_exit_code=$?
if test "$git_exit_code" -ne "0"; then
echo "::error ::upstream_utils check failed. This is usually caused by a bad script or the copied files differing from what the script outputs. You can learn more about using upstream_utils to modify thirdparty libraries at https://github.com/wpilibsuite/allwpilib/blob/main/upstream_utils/README.md"
exit $git_exit_code
fi

View File

@@ -289,6 +289,7 @@ endif()
set(FILENAME_DEP_REPLACE "get_filename_component(SELF_DIR \"$\{CMAKE_CURRENT_LIST_FILE\}\" PATH)")
set(SELF_DIR "$\{SELF_DIR\}")
set(WPIUNITS_DEP_REPLACE_IMPL "find_dependency(wpiunits)")
set(WPIANNOTATIONS_DEP_REPLACE_IMPL "find_dependency(wpiannotations)")
set(WPIUTIL_DEP_REPLACE "find_dependency(wpiutil)")
add_subdirectory(wpiutil)
@@ -308,6 +309,10 @@ if(WITH_WPIMATH)
add_subdirectory(wpimath)
endif()
if(WITH_JAVA)
add_subdirectory(wpiannotations)
endif()
if(WITH_WPIUNITS AND NOT WITH_WPIMATH)
# In case of building wpiunits standalone
set(WPIUNITS_DEP_REPLACE ${WPIUNITS_DEP_REPLACE_IMPL})

View File

@@ -56,7 +56,7 @@ the consequences for any action they deem in violation of this Code of Conduct:
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
**Consequence**: A warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

View File

@@ -51,39 +51,6 @@ Have an idea to make WPILib better? Here's some steps to go from idea to impleme
WPILib uses modified Google style guides for both C++ and Java, which can be found in the [styleguide repository](https://github.com/wpilibsuite/styleguide). Autoformatters are available for many popular editors at https://github.com/google/styleguide. Running wpiformat is required for all contributions and is enforced by our continuous integration system.
While the library should be fully formatted according to the styles, additional elements of the style guide were not followed when the library was initially created. All new code should follow the guidelines. If you are looking for some easy ramp-up tasks, finding areas that don't follow the style guide and fixing them is very welcome.
### Math documentation
When writing math expressions in documentation, use https://www.unicodeit.net/ to convert LaTeX to a Unicode equivalent that's easier to read. Not all expressions will translate (e.g., superscripts of superscripts) so focus on making it readable by someone who isn't familiar with LaTeX. If content on multiple lines needs to be aligned in Doxygen/Javadoc comments (e.g., integration/summation limits, matrices packed with square brackets and superscripts for them), put them in @verbatim/@endverbatim blocks in Doxygen or `<pre>` tags in Javadoc so they render with monospace font.
The LaTeX to Unicode conversions can also be done locally via the unicodeit Python package. To install it, execute:
```bash
pip install --user unicodeit
```
Here's example usage:
```bash
$ python -m unicodeit.cli 'x_{k+1} = Ax_k + Bu_k'
xₖ₊₁ = Axₖ + Buₖ
```
On Linux, this process can be streamlined further by adding the following Bash function to your .bashrc (requires `wl-clipboard` on Wayland or `xclip` on X11):
```bash
# Converts LaTeX to Unicode, prints the result, and copies it to the clipboard
uc() {
if [ $WAYLAND_DISPLAY ]; then
python -m unicodeit.cli $@ | tee >(wl-copy -n)
else
python -m unicodeit.cli $@ | tee >(xclip -sel)
fi
}
```
Here's example usage:
```bash
$ uc 'x_{k+1} = Ax_k + Bu_k'
xₖ₊₁ = Axₖ + Buₖ
```
## Submitting Changes
### Pull Request Format

View File

@@ -59,8 +59,6 @@ Using Gradle makes building WPILib very straightforward. It only has a few depen
On macOS ARM, run `softwareupdate --install-rosetta`. This is necessary to be able to use the macOS x86 roboRIO toolchain on ARM.
On linux, run `sudo apt install gfortran`. This is necessary to be able to build WPIcal on linux platforms.
## Setup
Clone the WPILib repository and follow the instructions above for installing any required tooling. The build process uses versioning information from git. Downloading the source is not sufficient to run the build.

View File

@@ -15,12 +15,12 @@ rules_jvm_external_deps()
load("@rules_jvm_external//:defs.bzl", "maven_install")
maven_artifacts = [
"org.ejml:ejml-simple:0.43.1",
"com.fasterxml.jackson.core:jackson-annotations:2.15.2",
"com.fasterxml.jackson.core:jackson-core:2.15.2",
"com.fasterxml.jackson.core:jackson-databind:2.15.2",
"us.hebi.quickbuf:quickbuf-runtime:1.3.3",
"com.google.code.gson:gson:2.10.1",
"org.ejml:ejml-simple:0.44.0",
"com.fasterxml.jackson.core:jackson-annotations:2.19.2",
"com.fasterxml.jackson.core:jackson-core:2.19.2",
"com.fasterxml.jackson.core:jackson-databind:2.19.2",
"us.hebi.quickbuf:quickbuf-runtime:1.4",
"com.google.code.gson:gson:2.13.1",
]
maven_install(

View File

@@ -152,9 +152,10 @@ public class AprilTagFieldLayout {
var pose =
switch (origin) {
case kBlueAllianceWallRightSide -> Pose3d.kZero;
case kRedAllianceWallRightSide -> new Pose3d(
new Translation3d(m_fieldDimensions.fieldLength, m_fieldDimensions.fieldWidth, 0),
new Rotation3d(0, 0, Math.PI));
case kRedAllianceWallRightSide ->
new Pose3d(
new Translation3d(m_fieldDimensions.fieldLength, m_fieldDimensions.fieldWidth, 0),
new Rotation3d(0, 0, Math.PI));
};
setOrigin(pose);
}

View File

@@ -24,7 +24,6 @@ import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
@SuppressWarnings("PMD.MutableStaticState")
class AprilTagDetectorTest {
@SuppressWarnings("MemberName")
AprilTagDetector detector;

View File

@@ -181,7 +181,11 @@ tasks.register('deployStatic') {
model {
components {
benchmarkCpp(NativeExecutableSpec) {
targetBuildTypes 'debug'
if (project.hasProperty('ciDebugOnly')) {
targetBuildTypes 'debug'
} else {
targetBuildTypes 'release'
}
sources {
cpp {
source {
@@ -235,7 +239,11 @@ model {
}
}
benchmarkCppStatic(NativeExecutableSpec) {
targetBuildTypes 'debug'
if (project.hasProperty('ciDebugOnly')) {
targetBuildTypes 'debug'
} else {
targetBuildTypes 'release'
}
nativeUtils.excludeBinariesFromStrip(it)
sources {
cpp {

View File

@@ -22,6 +22,7 @@ void BM_Transform(benchmark::State& state) {
auto transform = pose2 - pose1;
return units::math::hypot(transform.X(), transform.Y()).value();
}};
// NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
for (auto _ : state) {
traveler.Solve(poses, iterations);
}
@@ -33,6 +34,7 @@ void BM_Twist(benchmark::State& state) {
auto twist = pose1.Log(pose2);
return units::math::hypot(twist.dx, twist.dy).value();
}};
// NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
for (auto _ : state) {
traveler.Solve(poses, iterations);
}

View File

@@ -13,14 +13,14 @@ plugins {
id 'edu.wpi.first.wpilib.versioning.WPILibVersioningPlugin' version '2023.0.1'
id 'edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin' version '2020.2'
id 'edu.wpi.first.NativeUtils' apply false
id 'edu.wpi.first.GradleJni' version '1.1.0'
id 'edu.wpi.first.GradleJni' version '1.2.0'
id 'edu.wpi.first.GradleVsCode'
id 'idea'
id 'visual-studio'
id 'net.ltgt.errorprone' version '3.1.0' apply false
id 'com.gradleup.shadow' version '8.3.4' apply false
id 'com.diffplug.spotless' version '6.20.0' apply false
id 'com.github.spotbugs' version '6.0.2' apply false
id 'net.ltgt.errorprone' version '4.3.0' apply false
id 'com.gradleup.shadow' version '9.1.0' apply false
id 'com.diffplug.spotless' version '8.0.0' apply false
id 'com.github.spotbugs' version '6.4.2' apply false
}
wpilibVersioning.buildServerMode = project.hasProperty('buildServer')
@@ -39,11 +39,11 @@ allprojects {
}
}
buildScan {
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
termsOfServiceAgree = 'yes'
publishAlways()
develocity {
buildScan {
termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use"
termsOfUseAgree = "yes"
}
}
import com.github.spotbugs.snom.Effort
@@ -81,7 +81,7 @@ task libraryBuild() {}
build.dependsOn outputVersions
task copyAllOutputs(type: Copy) {
destinationDir outputsFolder
destinationDir = outputsFolder
}
build.dependsOn copyAllOutputs
@@ -170,5 +170,5 @@ ext.getCurrentArch = {
}
wrapper {
gradleVersion = '8.11'
gradleVersion = '8.14.3'
}

View File

@@ -5,9 +5,9 @@ repositories {
url = 'https://frcmaven.wpi.edu/artifactory/ex-gradle'
}
mavenCentral()
url "https://plugins.gradle.org/m2/"
url = "https://plugins.gradle.org/m2/"
}
}
dependencies {
implementation "edu.wpi.first:native-utils:2025.9.1"
implementation "edu.wpi.first:native-utils:2026.0.0"
}

View File

@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.code.gson:gson:2.13.1'
implementation project(':wpiutil')
implementation project(':wpinet')

View File

@@ -46,7 +46,7 @@ public final class CameraServer {
private static final String kPublishName = "/CameraPublisher";
private static final class PropertyPublisher implements AutoCloseable {
@SuppressWarnings({"PMD.MissingBreakInSwitch", "PMD.ImplicitSwitchFallThrough", "fallthrough"})
@SuppressWarnings("fallthrough")
PropertyPublisher(NetworkTable table, VideoEvent event) {
String name;
String infoName;
@@ -66,7 +66,7 @@ public final class CameraServer {
break;
case kEnum:
m_choicesTopic = table.getStringArrayTopic(infoName + "/choices");
// fall through
// fallthrough
case kInteger:
m_integerValueEntry = table.getIntegerTopic(name).getEntry(0);
m_minPublisher = table.getIntegerTopic(infoName + "/min").publish();

View File

@@ -13,6 +13,7 @@ import org.opencv.core.Mat;
* @see VisionRunner
* @see VisionThread
*/
@FunctionalInterface
public interface VisionPipeline {
/**
* Processes the image input and sets the result objects. Implementations should make these

View File

@@ -5,7 +5,6 @@
#include <opencv2/core/core.hpp>
#include <wpi/print.h>
#include "cscore.h"
#include "cscore_cv.h"
int main() {

View File

@@ -68,8 +68,9 @@ public class CvSource extends ImageSource {
case 2 -> PixelFormat.kYUYV; // 2 channels is assumed YUYV
case 3 -> PixelFormat.kBGR; // 3 channels is assumed BGR
case 4 -> PixelFormat.kBGRA; // 4 channels is assumed BGRA
default -> throw new VideoException(
"Unable to get pixel format for " + channels + " channels");
default ->
throw new VideoException(
"Unable to get pixel format for " + channels + " channels");
};
putFrame(finalImage, format, true);

View File

@@ -773,8 +773,7 @@ void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) {
lastFrameTime = thisFrameTime;
double timestamp = lastFrameTime / 1000000.0;
header.clear();
oss << "\r\n--" BOUNDARY "\r\n"
<< "Content-Type: image/jpeg\r\n";
oss << "\r\n--" BOUNDARY "\r\n" << "Content-Type: image/jpeg\r\n";
wpi::print(oss, "Content-Length: {}\r\n", size);
wpi::print(oss, "X-Timestamp: {}\r\n", timestamp);
oss << "\r\n";

View File

@@ -44,7 +44,7 @@ static O* ConvertToC(std::vector<I>&& in, int* count) {
// retain vector at end of returned array
alignas(T) unsigned char buf[sizeof(T)];
new (buf) T(std::move(in));
std::memcpy(out + size * sizeof(O), buf, sizeof(T));
std::memcpy(out + size, buf, sizeof(T));
return out;
}
@@ -392,7 +392,7 @@ void CS_FreeEvents(CS_Event* arr, int count) {
// destroy vector saved at end of array
using T = std::vector<cs::RawEvent>;
alignas(T) unsigned char buf[sizeof(T)];
std::memcpy(buf, arr + count * sizeof(CS_Event), sizeof(T));
std::memcpy(buf, arr + count, sizeof(T));
reinterpret_cast<T*>(buf)->~T();
std::free(arr);

View File

@@ -707,8 +707,9 @@ void UsbCameraImpl::DeviceCacheProperty(
}
NotifyPropertyCreated(*rawIndex, *rawPropPtr);
if (perPropPtr && perIndex)
if (perPropPtr && perIndex) {
NotifyPropertyCreated(*perIndex, *perPropPtr);
}
}
CS_StatusValue UsbCameraImpl::DeviceProcessCommand(

View File

@@ -31,7 +31,7 @@ model {
// Create the ZIP.
def task = project.tasks.create("copyDataLogToolExecutable" + binary.targetPlatform.operatingSystem.name + binary.targetPlatform.architecture.name, Zip) {
description("Copies the DataLogTool executable to the outputs directory.")
description = "Copies the DataLogTool executable to the outputs directory."
destinationDirectory = outputsFolder
archiveBaseName = zipBaseName
@@ -117,7 +117,7 @@ model {
artifactId = baseArtifactId
groupId = artifactGroupId
version wpilibVersioning.version.get()
version = wpilibVersioning.version.get()
}
}
}

View File

@@ -1,6 +1,6 @@
plugins {
id 'java'
id "org.ysb33r.doxygen" version "1.0.4"
id "org.ysb33r.doxygen" version "2.0.0"
}
evaluationDependsOn(':apriltag')
@@ -9,6 +9,7 @@ evaluationDependsOn(':cscore')
evaluationDependsOn(':epilogue-runtime')
evaluationDependsOn(':hal')
evaluationDependsOn(':ntcore')
evaluationDependsOn(':wpiannotations')
evaluationDependsOn(':wpilibNewCommands')
evaluationDependsOn(':wpilibc')
evaluationDependsOn(':wpilibj')
@@ -51,23 +52,27 @@ doxygen {
// See below maven and https://doxygen.nl/download.html for provided binaries
// Ensure theme.css (from https://github.com/jothepro/doxygen-awesome-css) is compatible with
// doxygen version when updating
executables {
doxygen {
// Note: has no effect if not on an x86_64 platform - you need to have a global install available on your
// PATH for the doxygen plugin to run
executableByVersion('1.12.0')
String arch = System.getProperty("os.arch");
if (arch.equals("x86_64") || arch.equals("amd64")) {
executables {
doxygen {
executableByVersion('1.12.0')
String arch = System.getProperty("os.arch");
if (!(arch.equals("x86_64") || arch.equals("amd64"))) {
// Search for a local doxygen install
executableBySearchPath('doxygen')
}
}
}
}
doxygen {
template 'Doxyfile'
doxygen.sourceSets.main {
template = 'Doxyfile'
cppProjectZips.each {
dependsOn it
source it.source
doxygenDox.dependsOn it
sources it.source
it.ext.includeDirs.each {
cppIncludeRoots.add(it.absolutePath)
}
@@ -77,66 +82,19 @@ doxygen {
// apriltag
exclude 'apriltag_pose.h'
// Eigen
exclude 'Eigen/**'
exclude 'unsupported/**'
// LLVM
exclude 'wpi/AlignOf.h'
exclude 'wpi/Casting.h'
exclude 'wpi/Chrono.h'
exclude 'wpi/Compiler.h'
exclude 'wpi/ConvertUTF.h'
exclude 'wpi/DenseMap.h'
exclude 'wpi/DenseMapInfo.h'
exclude 'wpi/Endian.h'
exclude 'wpi/EpochTracker.h'
exclude 'wpi/Errc.h'
exclude 'wpi/Errno.h'
exclude 'wpi/ErrorHandling.h'
exclude 'wpi/bit.h'
exclude 'wpi/fs.h'
exclude 'wpi/FunctionExtras.h'
exclude 'wpi/function_ref.h'
exclude 'wpi/Hashing.h'
exclude 'wpi/iterator.h'
exclude 'wpi/iterator_range.h'
exclude 'wpi/ManagedStatic.h'
exclude 'wpi/MapVector.h'
exclude 'wpi/MathExtras.h'
exclude 'wpi/MemAlloc.h'
exclude 'wpi/PointerIntPair.h'
exclude 'wpi/PointerLikeTypeTraits.h'
exclude 'wpi/PointerUnion.h'
exclude 'wpi/raw_os_ostream.h'
exclude 'wpi/raw_ostream.h'
exclude 'wpi/SmallPtrSet.h'
exclude 'wpi/SmallSet.h'
exclude 'wpi/SmallString.h'
exclude 'wpi/SmallVector.h'
exclude 'wpi/StringExtras.h'
exclude 'wpi/StringMap.h'
exclude 'wpi/SwapByteOrder.h'
exclude 'wpi/type_traits.h'
exclude 'wpi/VersionTuple.h'
exclude 'wpi/WindowsError.h'
// fmtlib
exclude 'fmt/**'
// libuv
exclude 'uv.h'
exclude 'uv/**'
exclude 'wpinet/uv/**'
// json
exclude 'wpi/adl_serializer.h'
exclude 'wpi/byte_container_with_subtype.h'
exclude 'wpi/detail/**'
exclude 'wpi/json.h'
exclude 'wpi/json_fwd.h'
exclude 'wpi/ordered_map.h'
exclude 'wpi/thirdparty/**'
// mpack
exclude 'wpi/mpack.h'
@@ -168,8 +126,9 @@ doxygen {
tasks.register("zipCppDocs", Zip) {
archiveBaseName = zipBaseNameCpp
archiveVersion = ""
destinationDirectory = outputsFolder
dependsOn doxygen
dependsOn doxygenDox
from ("$buildDir/docs/doxygen/html")
into '/'
}
@@ -177,7 +136,7 @@ tasks.register("zipCppDocs", Zip) {
// Java
configurations {
javaSource {
transitive false
transitive = false
}
}
@@ -206,6 +165,7 @@ task generateJavaDocs(type: Javadoc) {
"-edu.wpi.first.math.system.plant.proto," +
"-edu.wpi.first.math.system.plant.struct," +
"-edu.wpi.first.math.trajectory.proto," +
"-edu.wpi.first.math.trajectory.struct," +
// The .measure package contains generated source files for which automatic javadoc
// generation is very difficult to do meaningfully.
"-edu.wpi.first.units.measure", true)
@@ -219,6 +179,7 @@ task generateJavaDocs(type: Javadoc) {
source project(':epilogue-runtime').sourceSets.main.java
source project(':hal').sourceSets.main.java
source project(':ntcore').sourceSets.main.java
source project(':wpiannotations').sourceSets.main.java
source project(':wpilibNewCommands').sourceSets.main.java
source project(':wpilibj').sourceSets.main.java
source project(':wpimath').sourceSets.main.java
@@ -246,6 +207,7 @@ task generateJavaDocs(type: Javadoc) {
tasks.register("zipJavaDocs", Zip) {
archiveBaseName = zipBaseNameJava
archiveVersion = ""
destinationDirectory = outputsFolder
dependsOn generateJavaDocs
from ("$buildDir/docs/javadoc")
@@ -265,15 +227,15 @@ publishing {
artifact zipJavaDocs
artifactId = "${baseArtifactIdJava}"
groupId artifactGroupIdJava
version wpilibVersioning.version.get()
groupId = artifactGroupIdJava
version = wpilibVersioning.version.get()
}
cpp(MavenPublication) {
artifact zipCppDocs
artifactId = "${baseArtifactIdCpp}"
groupId artifactGroupIdCpp
version wpilibVersioning.version.get()
groupId = artifactGroupIdCpp
version = wpilibVersioning.version.get()
}
}
}

View File

@@ -112,7 +112,8 @@ public class AnnotationProcessor extends AbstractProcessor {
new MeasureHandler(processingEnv),
new PrimitiveHandler(processingEnv),
new SupplierHandler(processingEnv),
new StructHandler(processingEnv), // prioritize struct over sendable
new StructHandler(processingEnv), // prioritize struct over sendable and protobuf
new ProtobufHandler(processingEnv), // then protobuf
new SendableHandler(processingEnv));
m_epiloguerGenerator = new EpilogueGenerator(processingEnv, customLoggers);

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
@@ -52,7 +53,7 @@ public class ArrayHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
// known to be an array type (assuming isLoggable is checked first); this is a safe cast
@@ -63,13 +64,17 @@ public class ArrayHandler extends ElementHandler {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", "
+ m_structHandler.structAccess(componentType)
+ ")";
} else {
// Primitive or string array
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}
}

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
@@ -38,7 +39,7 @@ public class CollectionHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
var componentType = ((DeclaredType) dataType).getTypeArguments().get(0);
@@ -46,12 +47,16 @@ public class CollectionHandler extends ElementHandler {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", "
+ m_structHandler.structAccess(componentType)
+ ")";
} else {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}
}

View File

@@ -7,6 +7,7 @@ package edu.wpi.first.epilogue.processor;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
@@ -27,7 +28,7 @@ public class ConfiguredLoggerHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
var loggerType =
m_customLoggers.entrySet().stream()
@@ -44,7 +45,7 @@ public class ConfiguredLoggerHandler extends ElementHandler {
+ ".tryUpdate(backend.getNested(\""
+ loggedName(element)
+ "\"), "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", Epilogue.getConfig().errorHandler)";
}
}

View File

@@ -117,9 +117,9 @@ public abstract class ElementHandler {
* @param element the element to generate the access for
* @return the generated access snippet
*/
public String elementAccess(Element element) {
public String elementAccess(Element element, TypeElement loggedClass) {
if (element instanceof VariableElement field) {
return fieldAccess(field);
return fieldAccess(field, loggedClass);
} else if (element instanceof ExecutableElement method) {
return methodAccess(method);
} else {
@@ -127,8 +127,20 @@ public abstract class ElementHandler {
}
}
private static String fieldAccess(VariableElement field) {
if (!field.getModifiers().contains(Modifier.PUBLIC)) {
private static String fieldAccess(VariableElement field, TypeElement loggedClass) {
var mods = field.getModifiers();
// To be directly accessible, the field needs to be:
// - public; or
// - protected or package-private, and declared by a superclass in the same package
// However, we can't cleanly access package information, so we'll always emit a VarHandle
// for any field declared in a superclass unless it's public and we know we can read it.
boolean isVarHandle =
field.getEnclosingElement().equals(loggedClass)
? mods.contains(Modifier.PRIVATE)
: !mods.contains(Modifier.PUBLIC);
if (isVarHandle) {
// ((com.example.Foo) $fooField.get(object))
// Extra parentheses so cast evaluates before appended methods
// (e.g. when appending .getAsDouble())
@@ -136,7 +148,7 @@ public abstract class ElementHandler {
if (type.getKind() == TypeKind.TYPEVAR) {
type = ((TypeVariable) type).getUpperBound();
}
return "((" + type.toString() + ") $" + field.getSimpleName() + ".get(object))";
return "((" + type.toString() + ") " + LoggerGenerator.varHandleName(field) + ".get(object))";
} else {
// object.fooField
return "object." + field.getSimpleName();
@@ -171,5 +183,5 @@ public abstract class ElementHandler {
* @param element the field or method element to generate the logger call for
* @return the generated log invocation
*/
public abstract String logInvocation(Element element);
public abstract String logInvocation(Element element, TypeElement loggedClass);
}

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class EnumHandler extends ElementHandler {
@@ -27,7 +28,11 @@ public class EnumHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -39,7 +39,7 @@ public class LoggableHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
TypeMirror dataType = dataType(element);
var declaredType =
m_processingEnv
@@ -61,7 +61,7 @@ public class LoggableHandler extends ElementHandler {
// If there are no known loggable subtypes, return just the single logger call
if (size == 1) {
return generateLoggerCall(element, declaredType, elementAccess(element));
return generateLoggerCall(element, declaredType, elementAccess(element, loggedClass));
}
// Otherwise, generate an if-else chain to compare the element with its known loggable subtypes
@@ -73,7 +73,7 @@ public class LoggableHandler extends ElementHandler {
StringBuilder builder = new StringBuilder();
// Cache the value in a variable so it's only read once
builder.append("var %s = %s;\n".formatted(varName, elementAccess(element)));
builder.append("var %s = %s;\n".formatted(varName, elementAccess(element, loggedClass)));
for (int i = 0; i < size; i++) {
TypeElement type = loggableSubtypes.get(i);

View File

@@ -18,9 +18,11 @@ import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -185,7 +187,21 @@ public class LoggerGenerator {
var loggerFile = m_processingEnv.getFiler().createSourceFile(loggerClassName);
var varHandleFields =
loggableFields.stream().filter(e -> !e.getModifiers().contains(Modifier.PUBLIC)).toList();
loggableFields.stream()
.filter(
e -> {
if (e.getEnclosingElement().equals(clazz)) {
// The generated logger is in the same package as the logged class, so the
// only fields it can't read are private ones.
return e.getModifiers().contains(Modifier.PRIVATE);
} else {
// Logging from a superclass. Can only read public fields, unless the superclass
// is in the same package, in which case protected and package-private fields
// are also readable.
return !e.getModifiers().contains(Modifier.PUBLIC);
}
})
.toList();
boolean requiresVarHandles = !varHandleFields.isEmpty();
try (var out = new PrintWriter(loggerFile.openWriter())) {
@@ -214,41 +230,67 @@ public class LoggerGenerator {
+ "> {");
if (requiresVarHandles) {
for (var varHandleField : varHandleFields) {
for (var privateField : varHandleFields) {
// This field needs a VarHandle to access.
// Cache it in the class to avoid lookups
out.println(" private static final VarHandle $" + varHandleField.getSimpleName() + ";");
out.printf(
" // Accesses private or superclass field %s.%s%n",
privateField.getEnclosingElement(), privateField.getSimpleName());
out.printf(" private static final VarHandle %s;%n", varHandleName(privateField));
}
out.println();
}
var classReference = simpleClassName + ".class";
// Static initializer block to load VarHandles and reflection fields
if (requiresVarHandles) {
out.println(" static {");
out.println(" try {");
out.println(
" var lookup = MethodHandles.privateLookupIn("
+ classReference
+ ", MethodHandles.lookup());");
for (var varHandleField : varHandleFields) {
var fieldName = varHandleField.getSimpleName();
out.println(
" $"
+ fieldName
+ " = lookup.findVarHandle("
+ classReference
+ ", \""
+ fieldName
+ "\", "
+ m_processingEnv.getTypeUtils().erasure(varHandleField.asType())
+ ".class);");
}
out.println(" try {");
out.println(" var rootLookup = MethodHandles.lookup();");
// Group private fields by class, then generate a private lookup for each class
// and a VarHandle for each field using that lookup. Sorting and then collecting into a
// LinkedHashMap gives deterministic output ordering (mostly for tests, which check exact
// file contents, but also results in less churn when regenerating files for users who like
// to read the generated logger classes).
//
// This lets us read private fields from superclasses.
Map<Element, List<VariableElement>> privateFieldsByClass =
varHandleFields.stream()
.sorted(Comparator.comparing(e -> e.getSimpleName().toString()))
.collect(
Collectors.groupingBy(
VariableElement::getEnclosingElement,
LinkedHashMap::new,
Collectors.toList()));
privateFieldsByClass.forEach(
(enclosingClass, fields) -> {
String className = enclosingClass.toString();
String lookupName = "lookup$$" + className.replace(".", "_");
out.printf(
" var %s = MethodHandles.privateLookupIn(%s.class, rootLookup);%n",
lookupName, className);
for (var field : fields) {
var fieldname = field.getSimpleName();
out.printf(
" %s = %s.findVarHandle(%s.class, \"%s\", %s.class);%n",
varHandleName(field),
lookupName,
className,
fieldname,
m_processingEnv.getTypeUtils().erasure(field.asType()));
}
});
out.println(" } catch (ReflectiveOperationException e) {");
out.println(
" throw new RuntimeException("
+ "\"[EPILOGUE] Could not load private fields for logging!\", e);");
out.println(" }");
out.println(" }");
out.println();
}
@@ -300,7 +342,7 @@ public class LoggerGenerator {
// to be logged. For example, the sendable handler consumes all sendable types
// but does not log commands or subsystems, to prevent excessive warnings about
// unloggable commands.
var logInvocation = h.logInvocation(loggableElement);
var logInvocation = h.logInvocation(loggableElement, clazz);
if (logInvocation != null) {
out.println(logInvocation.indent(6).stripTrailing() + ";");
}
@@ -315,6 +357,18 @@ public class LoggerGenerator {
}
}
/**
* Generates the name of a VarHandle for access to the given field. The VarHandle variable's name
* is guaranteed to be unique.
*
* @param field The field to generate a VarHandle for
* @return The name of the generated VarHandle variable
*/
public static String varHandleName(VariableElement field) {
return "$%s_%s"
.formatted(field.getEnclosingElement().toString().replace(".", "_"), field.getSimpleName());
}
private void collectLoggables(
TypeElement clazz, List<VariableElement> fields, List<ExecutableElement> methods) {
var config = clazz.getAnnotation(Logged.class);

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class MeasureHandler extends ElementHandler {
@@ -30,8 +31,12 @@ public class MeasureHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
// EpilogueBackend has builtin support for logging measures
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -16,6 +16,7 @@ import static javax.lang.model.type.TypeKind.SHORT;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class PrimitiveHandler extends ElementHandler {
@@ -35,7 +36,11 @@ public class PrimitiveHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -0,0 +1,102 @@
// 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.epilogue.processor;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/**
* Supports protobuf serializable types. Protobuf-serializable types are loggable if they have a
* public static final {@code proto} field of a type that inherits from {@code Protobuf}.
*/
public class ProtobufHandler extends ElementHandler {
private final TypeMirror m_serializable;
private final TypeElement m_protobufType;
private final Types m_typeUtils;
private final Elements m_elementUtils;
protected ProtobufHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
m_serializable =
processingEnv
.getElementUtils()
.getTypeElement("edu.wpi.first.util.protobuf.ProtobufSerializable")
.asType();
m_protobufType =
processingEnv.getElementUtils().getTypeElement("edu.wpi.first.util.protobuf.Protobuf");
m_typeUtils = processingEnv.getTypeUtils();
m_elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean isLoggable(Element element) {
return isLoggableType(dataType(element));
}
/**
* Checks if a type is protobuf-serializable: implements the ProtobufSerializable marker interface
* and has a `public static final proto` field of a type that inherits from Protobuf with a
* compatible generic type bound.
*
* @param type The type to check
* @return true if the type is protobuf-serializable, false otherwise
*/
public boolean isLoggableType(TypeMirror type) {
var serializableType = m_typeUtils.erasure(type);
var typeElement = m_elementUtils.getTypeElement(serializableType.toString());
if (typeElement == null) {
return false;
}
// eg `Protobuf<Rotation2d, ?>` instead of the raw `Protobuf` type. The message type doesn't
// really matter here; we can leave it as a wildcard.
var sharpProtobufType =
m_typeUtils.getDeclaredType(
m_protobufType,
typeElement.asType(), // the serializable type
m_typeUtils.getWildcardType(
m_elementUtils.getTypeElement("us.hebi.quickbuf.ProtoMessage").asType(), null));
boolean hasProto =
typeElement.getEnclosedElements().stream()
.filter(e -> e instanceof VariableElement)
.map(e -> (VariableElement) e)
.anyMatch(
field -> {
var nameMatch = field.getSimpleName().contentEquals("proto");
var modifiersMatch =
field
.getModifiers()
.containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
var typeMatch =
m_typeUtils.isAssignable(
m_typeUtils.erasure(field.asType()), sharpProtobufType);
return nameMatch && modifiersMatch && typeMatch;
});
return m_typeUtils.isAssignable(type, m_serializable) && hasProto;
}
public String protoAccess(TypeMirror serializableType) {
var className = m_typeUtils.erasure(serializableType).toString();
return className + ".proto";
}
@Override
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\"%s\", %s, %s)"
.formatted(
loggedName(element),
elementAccess(element, loggedClass),
protoAccess(dataType(element)));
}
}

View File

@@ -44,7 +44,7 @@ public class SendableHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
var dataType = dataType(element);
// Do not log commands or subsystems via their sendable implementations
@@ -66,7 +66,7 @@ public class SendableHandler extends ElementHandler {
return "logSendable(backend.getNested(\""
+ loggedName(element)
+ "\"), "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ")";
}
}

View File

@@ -4,14 +4,26 @@
package edu.wpi.first.epilogue.processor;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/**
* Supports struct serializable types. Struct-serializable types are loggable if they have a public
* static final {@code struct} field of a type that inherits from {@code Struct}.
*/
public class StructHandler extends ElementHandler {
private final TypeMirror m_serializable;
private final TypeElement m_structType;
private final Types m_typeUtils;
private final Elements m_elementUtils;
protected StructHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
@@ -20,16 +32,57 @@ public class StructHandler extends ElementHandler {
.getElementUtils()
.getTypeElement("edu.wpi.first.util.struct.StructSerializable")
.asType();
m_structType =
processingEnv.getElementUtils().getTypeElement("edu.wpi.first.util.struct.Struct");
m_typeUtils = processingEnv.getTypeUtils();
m_elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean isLoggable(Element element) {
return m_typeUtils.isAssignable(dataType(element), m_serializable);
return isLoggableType(dataType(element));
}
/**
* Checks if a type is struct-serializable: implements the StructSerializable marker interface and
* has a `public static final struct` field of a type that inherits from Struct with a compatible
* generic type bound.
*
* @param type The type to check
* @return true if the type is struct-serializable, false otherwise
*/
public boolean isLoggableType(TypeMirror type) {
return m_typeUtils.isAssignable(type, m_serializable);
TypeMirror serializableType;
if (type instanceof ArrayType arr) {
serializableType = arr.getComponentType();
} else {
serializableType = m_typeUtils.erasure(type);
}
var typeElement = m_elementUtils.getTypeElement(serializableType.toString());
if (typeElement == null) {
return false;
}
// eg `Struct<Rotation2d>` instead of the raw `Struct` type
var sharpStructType = m_typeUtils.getDeclaredType(m_structType, typeElement.asType());
boolean hasStruct =
typeElement.getEnclosedElements().stream()
.filter(e -> e instanceof VariableElement)
.map(e -> (VariableElement) e)
.anyMatch(
field -> {
var nameMatch = field.getSimpleName().contentEquals("struct");
var modifiersMatch =
field
.getModifiers()
.containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
var typeMatch =
m_typeUtils.isAssignable(
m_typeUtils.erasure(field.asType()), sharpStructType);
return nameMatch && modifiersMatch && typeMatch;
});
return m_typeUtils.isAssignable(type, m_serializable) && hasStruct;
}
public String structAccess(TypeMirror serializableType) {
@@ -38,11 +91,11 @@ public class StructHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ elementAccess(element, loggedClass)
+ ", "
+ structAccess(dataType(element))
+ ")";

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public class SupplierHandler extends ElementHandler {
@@ -42,15 +43,19 @@ public class SupplierHandler extends ElementHandler {
}
@Override
public String logInvocation(Element element) {
return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
public String logInvocation(Element element, TypeElement loggedClass) {
return "backend.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element, loggedClass)
+ ")";
}
@Override
public String elementAccess(Element element) {
public String elementAccess(Element element, TypeElement loggedClass) {
var typeUtils = m_processingEnv.getTypeUtils();
var dataType = dataType(element);
String base = super.elementAccess(element);
String base = super.elementAccess(element, loggedClass);
if (typeUtils.isAssignable(dataType, m_booleanSupplier)) {
return base + ".getAsBoolean()";

View File

@@ -8,5 +8,6 @@ java_library(
"//ntcore:networktables-java",
"//wpiunits",
"//wpiutil:wpiutil-java",
"@maven//:us_hebi_quickbuf_quickbuf_runtime",
],
)

View File

@@ -13,4 +13,5 @@ dependencies {
api(project(':ntcore'))
api(project(':wpiutil'))
api(project(':wpiunits'))
testImplementation(project(':wpimath')) // for convenient protobuf types
}

View File

@@ -106,14 +106,13 @@ public abstract class ClassSpecificLogger<T> {
return;
}
var builder =
m_sendables.computeIfAbsent(
sendable,
s -> {
var b = new LogBackedSendableBuilder(backend);
s.initSendable(b);
return b;
});
builder.update();
if (m_sendables.containsKey(sendable)) {
m_sendables.get(sendable).update();
} else {
var builder = new LogBackedSendableBuilder(backend);
sendable.initSendable(builder);
m_sendables.put(sendable, builder);
builder.update();
}
}
}

View File

@@ -6,8 +6,10 @@ package edu.wpi.first.epilogue.logging;
import edu.wpi.first.units.Measure;
import edu.wpi.first.units.Unit;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.util.Collection;
import us.hebi.quickbuf.ProtoMessage;
/** A backend is a generic interface for Epilogue to log discrete data points. */
public interface EpilogueBackend {
@@ -193,6 +195,17 @@ public interface EpilogueBackend {
log(identifier, array, struct);
}
/**
* Logs a protobuf-serializable object.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
* @param proto the protobuf to use to serialize the data
* @param <P> the protobuf-serializable type
* @param <M> the protobuf message type
*/
<P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto);
/**
* Logs a measurement's value in terms of its base unit.
*

View File

@@ -16,21 +16,28 @@ import edu.wpi.first.util.datalog.FloatArrayLogEntry;
import edu.wpi.first.util.datalog.FloatLogEntry;
import edu.wpi.first.util.datalog.IntegerArrayLogEntry;
import edu.wpi.first.util.datalog.IntegerLogEntry;
import edu.wpi.first.util.datalog.ProtobufLogEntry;
import edu.wpi.first.util.datalog.RawLogEntry;
import edu.wpi.first.util.datalog.StringArrayLogEntry;
import edu.wpi.first.util.datalog.StringLogEntry;
import edu.wpi.first.util.datalog.StructArrayLogEntry;
import edu.wpi.first.util.datalog.StructLogEntry;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import us.hebi.quickbuf.ProtoMessage;
/** A backend implementation that saves information to a WPILib {@link DataLog} file on disk. */
public class FileBackend implements EpilogueBackend {
private final DataLog m_dataLog;
private final Map<String, DataLogEntry> m_entries = new HashMap<>();
private final Map<String, NestedBackend> m_subLoggers = new HashMap<>();
private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
private final Set<Protobuf<?, ?>> m_seenProtos = new HashSet<>();
/**
* Creates a new file-based backend.
@@ -43,7 +50,13 @@ public class FileBackend implements EpilogueBackend {
@Override
public EpilogueBackend getNested(String path) {
return m_subLoggers.computeIfAbsent(path, k -> new NestedBackend(k, this));
if (!m_subLoggers.containsKey(path)) {
var nested = new NestedBackend(path, this);
m_subLoggers.put(path, nested);
return nested;
}
return m_subLoggers.get(path);
}
@SuppressWarnings("unchecked")
@@ -131,14 +144,45 @@ public class FileBackend implements EpilogueBackend {
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S value, Struct<S> struct) {
m_dataLog.addSchema(struct);
getEntry(identifier, (log, k) -> StructLogEntry.create(log, k, struct)).append(value);
// DataLog.addSchema has checks that we're able to skip, avoiding allocations
if (m_seenSchemas.add(struct)) {
m_dataLog.addSchema(struct);
}
if (!m_entries.containsKey(identifier)) {
m_entries.put(identifier, StructLogEntry.create(m_dataLog, identifier, struct));
}
((StructLogEntry<S>) m_entries.get(identifier)).append(value);
}
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S[] value, Struct<S> struct) {
m_dataLog.addSchema(struct);
getEntry(identifier, (log, k) -> StructArrayLogEntry.create(log, k, struct)).append(value);
// DataLog.addSchema has checks that we're able to skip, avoiding allocations
if (m_seenSchemas.add(struct)) {
m_dataLog.addSchema(struct);
}
if (!m_entries.containsKey(identifier)) {
m_entries.put(identifier, StructArrayLogEntry.create(m_dataLog, identifier, struct));
}
((StructArrayLogEntry<S>) m_entries.get(identifier)).append(value);
}
@Override
@SuppressWarnings("unchecked")
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
// DataLog.addSchema has checks that we're able to skip, avoiding allocations
if (m_seenProtos.add(proto)) {
m_dataLog.addSchema(proto);
}
if (!m_entries.containsKey(identifier)) {
m_entries.put(identifier, ProtobufLogEntry.create(m_dataLog, identifier, proto));
}
((ProtobufLogEntry<P>) m_entries.get(identifier)).append(value);
}
}

View File

@@ -4,11 +4,13 @@
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import us.hebi.quickbuf.ProtoMessage;
/**
* A backend implementation that only logs data when it changes. Useful for keeping bandwidth and
@@ -40,7 +42,13 @@ public class LazyBackend implements EpilogueBackend {
@Override
public EpilogueBackend getNested(String path) {
return m_subLoggers.computeIfAbsent(path, k -> new NestedBackend(k, this));
if (!m_subLoggers.containsKey(path)) {
var nested = new NestedBackend(path, this);
m_subLoggers.put(path, nested);
return nested;
}
return m_subLoggers.get(path);
}
@Override
@@ -237,4 +245,17 @@ public class LazyBackend implements EpilogueBackend {
m_previousValues.put(identifier, value.clone());
m_backend.log(identifier, value, struct);
}
@Override
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
var previous = m_previousValues.get(identifier);
if (Objects.equals(previous, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_backend.log(identifier, value, proto);
}
}

View File

@@ -19,7 +19,6 @@ import java.util.function.LongSupplier;
import java.util.function.Supplier;
/** A sendable builder implementation that sends data to a {@link EpilogueBackend}. */
@SuppressWarnings("PMD.CouplingBetweenObjects") // most methods simply delegate to the backend
public class LogBackedSendableBuilder implements SendableBuilder {
private final EpilogueBackend m_backend;
private final Collection<Runnable> m_updates = new ArrayList<>();

View File

@@ -4,10 +4,12 @@
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import us.hebi.quickbuf.ProtoMessage;
/**
* A backend implementation that delegates to other backends. Helpful for simultaneous logging to
@@ -24,7 +26,13 @@ public class MultiBackend implements EpilogueBackend {
@Override
public EpilogueBackend getNested(String path) {
return m_nestedBackends.computeIfAbsent(path, k -> new NestedBackend(k, this));
if (!m_nestedBackends.containsKey(path)) {
var nested = new NestedBackend(path, this);
m_nestedBackends.put(path, nested);
return nested;
}
return m_nestedBackends.get(path);
}
@Override
@@ -131,4 +139,11 @@ public class MultiBackend implements EpilogueBackend {
backend.log(identifier, value, struct);
}
}
@Override
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
for (EpilogueBackend backend : m_backends) {
backend.log(identifier, value, proto);
}
}
}

View File

@@ -13,15 +13,21 @@ import edu.wpi.first.networktables.FloatPublisher;
import edu.wpi.first.networktables.IntegerArrayPublisher;
import edu.wpi.first.networktables.IntegerPublisher;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.ProtobufPublisher;
import edu.wpi.first.networktables.Publisher;
import edu.wpi.first.networktables.RawPublisher;
import edu.wpi.first.networktables.StringArrayPublisher;
import edu.wpi.first.networktables.StringPublisher;
import edu.wpi.first.networktables.StructArrayPublisher;
import edu.wpi.first.networktables.StructPublisher;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import us.hebi.quickbuf.ProtoMessage;
/**
* A backend implementation that sends data over network tables. Be careful when using this, since
@@ -32,61 +38,82 @@ public class NTEpilogueBackend implements EpilogueBackend {
private final Map<String, Publisher> m_publishers = new HashMap<>();
private final Map<String, NestedBackend> m_nestedBackends = new HashMap<>();
private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
private final Set<Protobuf<?, ?>> m_seenProtos = new HashSet<>();
private final Function<String, IntegerPublisher> m_createIntPublisher;
private final Function<String, FloatPublisher> m_createFloatPublisher;
private final Function<String, DoublePublisher> m_createDoublePublisher;
private final Function<String, BooleanPublisher> m_createBooleanPublisher;
private final Function<String, RawPublisher> m_createRawPublisher;
private final Function<String, IntegerArrayPublisher> m_createIntegerArrayPublisher;
private final Function<String, FloatArrayPublisher> m_createFloatArrayPublisher;
private final Function<String, DoubleArrayPublisher> m_createDoubleArrayPublisher;
private final Function<String, BooleanArrayPublisher> m_createBooleanArrayPublisher;
private final Function<String, StringPublisher> m_createStringPublisher;
private final Function<String, StringArrayPublisher> m_createStringArrayPublisher;
/**
* Creates a logging backend that sends information to NetworkTables.
*
* @param nt the NetworkTable instance to use to send data to
*/
@SuppressWarnings("unchecked")
public NTEpilogueBackend(NetworkTableInstance nt) {
this.m_nt = nt;
m_createIntPublisher = identifier -> m_nt.getIntegerTopic(identifier).publish();
m_createFloatPublisher = identifier -> m_nt.getFloatTopic(identifier).publish();
m_createDoublePublisher = identifier -> m_nt.getDoubleTopic(identifier).publish();
m_createBooleanPublisher = identifier -> m_nt.getBooleanTopic(identifier).publish();
m_createRawPublisher = identifier -> m_nt.getRawTopic(identifier).publish("raw");
m_createIntegerArrayPublisher = identifier -> m_nt.getIntegerArrayTopic(identifier).publish();
m_createFloatArrayPublisher = identifier -> m_nt.getFloatArrayTopic(identifier).publish();
m_createDoubleArrayPublisher = identifier -> m_nt.getDoubleArrayTopic(identifier).publish();
m_createBooleanArrayPublisher = identifier -> m_nt.getBooleanArrayTopic(identifier).publish();
m_createStringPublisher = identifier -> m_nt.getStringTopic(identifier).publish();
m_createStringArrayPublisher = identifier -> m_nt.getStringArrayTopic(identifier).publish();
}
@Override
public EpilogueBackend getNested(String path) {
return m_nestedBackends.computeIfAbsent(path, k -> new NestedBackend(k, this));
if (!m_nestedBackends.containsKey(path)) {
var nested = new NestedBackend(path, this);
m_nestedBackends.put(path, nested);
return nested;
}
return m_nestedBackends.get(path);
}
@Override
public void log(String identifier, int value) {
((IntegerPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
.set(value);
((IntegerPublisher) m_publishers.computeIfAbsent(identifier, m_createIntPublisher)).set(value);
}
@Override
public void log(String identifier, long value) {
((IntegerPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
.set(value);
((IntegerPublisher) m_publishers.computeIfAbsent(identifier, m_createIntPublisher)).set(value);
}
@Override
public void log(String identifier, float value) {
((FloatPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatTopic(k).publish()))
.set(value);
((FloatPublisher) m_publishers.computeIfAbsent(identifier, m_createFloatPublisher)).set(value);
}
@Override
public void log(String identifier, double value) {
((DoublePublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleTopic(k).publish()))
((DoublePublisher) m_publishers.computeIfAbsent(identifier, m_createDoublePublisher))
.set(value);
}
@Override
public void log(String identifier, boolean value) {
((BooleanPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanTopic(k).publish()))
((BooleanPublisher) m_publishers.computeIfAbsent(identifier, m_createBooleanPublisher))
.set(value);
}
@Override
public void log(String identifier, byte[] value) {
((RawPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getRawTopic(k).publish("raw")))
.set(value);
((RawPublisher) m_publishers.computeIfAbsent(identifier, m_createRawPublisher)).set(value);
}
@Override
@@ -100,68 +127,96 @@ public class NTEpilogueBackend implements EpilogueBackend {
}
((IntegerArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
m_publishers.computeIfAbsent(identifier, m_createIntegerArrayPublisher))
.set(widened);
}
@Override
public void log(String identifier, long[] value) {
((IntegerArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
m_publishers.computeIfAbsent(identifier, m_createIntegerArrayPublisher))
.set(value);
}
@Override
public void log(String identifier, float[] value) {
((FloatArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatArrayTopic(k).publish()))
((FloatArrayPublisher) m_publishers.computeIfAbsent(identifier, m_createFloatArrayPublisher))
.set(value);
}
@Override
public void log(String identifier, double[] value) {
((DoubleArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleArrayTopic(k).publish()))
((DoubleArrayPublisher) m_publishers.computeIfAbsent(identifier, m_createDoubleArrayPublisher))
.set(value);
}
@Override
public void log(String identifier, boolean[] value) {
((BooleanArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanArrayTopic(k).publish()))
m_publishers.computeIfAbsent(identifier, m_createBooleanArrayPublisher))
.set(value);
}
@Override
public void log(String identifier, String value) {
((StringPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringTopic(k).publish()))
((StringPublisher) m_publishers.computeIfAbsent(identifier, m_createStringPublisher))
.set(value);
}
@Override
public void log(String identifier, String[] value) {
((StringArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringArrayTopic(k).publish()))
((StringArrayPublisher) m_publishers.computeIfAbsent(identifier, m_createStringArrayPublisher))
.set(value);
}
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S value, Struct<S> struct) {
m_nt.addSchema(struct);
((StructPublisher<S>)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStructTopic(k, struct).publish()))
.set(value);
// NetworkTableInstance.addSchema has checks that we're able to skip, avoiding allocations
if (m_seenSchemas.add(struct)) {
m_nt.addSchema(struct);
}
if (m_publishers.containsKey(identifier)) {
((StructPublisher<S>) m_publishers.get(identifier)).set(value);
} else {
StructPublisher<S> publisher = m_nt.getStructTopic(identifier, struct).publish();
m_publishers.put(identifier, publisher);
publisher.set(value);
}
}
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S[] value, Struct<S> struct) {
m_nt.addSchema(struct);
((StructArrayPublisher<S>)
m_publishers.computeIfAbsent(
identifier, k -> m_nt.getStructArrayTopic(k, struct).publish()))
.set(value);
// NetworkTableInstance.addSchema has checks that we're able to skip, avoiding allocations
if (m_seenSchemas.add(struct)) {
m_nt.addSchema(struct);
}
if (m_publishers.containsKey(identifier)) {
((StructArrayPublisher<S>) m_publishers.get(identifier)).set(value);
} else {
StructArrayPublisher<S> publisher = m_nt.getStructArrayTopic(identifier, struct).publish();
m_publishers.put(identifier, publisher);
publisher.set(value);
}
}
@Override
@SuppressWarnings("unchecked")
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
// NetworkTableInstance.addSchema has checks that we're able to skip, avoiding allocations
if (m_seenProtos.add(proto)) {
m_nt.addSchema(proto);
}
if (m_publishers.containsKey(identifier)) {
((ProtobufPublisher<P>) m_publishers.get(identifier)).set(value);
} else {
ProtobufPublisher<P> publisher = m_nt.getProtobufTopic(identifier, proto).publish();
m_publishers.put(identifier, publisher);
publisher.set(value);
}
}
}

View File

@@ -4,9 +4,11 @@
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.Map;
import us.hebi.quickbuf.ProtoMessage;
/**
* A backend that logs to an underlying backend, prepending all logged data with a specific prefix.
@@ -17,6 +19,15 @@ public class NestedBackend implements EpilogueBackend {
private final EpilogueBackend m_impl;
private final Map<String, NestedBackend> m_nestedBackends = new HashMap<>();
// String concatenation can be expensive, especially for deeply nested hierarchies with many
// logged fields. For example, logging a hypothetical Robot.elevator.io.getHeight() would result
// in "/Robot/" + "elevator/" + "io/" + "getHeight"; three concatenations and string and byte
// array allocations that need to be cleaned up by the GC. Caching the results means those
// allocations only occur once, resulting in no GC (the strings are always referenced in the
// cache), and minimal time costs (the String object caches its own hash code, so all we do is an
// O(1) table lookup per concatenation)
private final Map<String, String> m_prefixedIdentifiers = new HashMap<>();
/**
* Creates a new nested backed underneath another backend.
*
@@ -33,83 +44,114 @@ public class NestedBackend implements EpilogueBackend {
this.m_impl = impl;
}
/**
* Fast lookup to avoid redundant `m_prefix + identifier` concatenations. If the identifier has
* not been seen before, we compute the concatenation and cache the result for later invocations
* to read. This avoids redundantly recomputing the same concatenations every loop and
* significantly cuts down on the CPU and memory overhead of the Epilogue library.
*
* @param identifier The identifier to prepend with {@link #m_prefix}.
* @return The concatenated string.
*/
private String withPrefix(String identifier) {
// Using computeIfAbsent would result in a new lambda object allocation on every call
if (m_prefixedIdentifiers.containsKey(identifier)) {
return m_prefixedIdentifiers.get(identifier);
}
String result = m_prefix + identifier;
m_prefixedIdentifiers.put(identifier, result);
return result;
}
@Override
public EpilogueBackend getNested(String path) {
return m_nestedBackends.computeIfAbsent(path, k -> new NestedBackend(k, this));
if (!m_nestedBackends.containsKey(path)) {
var nested = new NestedBackend(path, this);
m_nestedBackends.put(path, nested);
return nested;
}
return m_nestedBackends.get(path);
}
@Override
public void log(String identifier, int value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, long value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, float value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, double value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, boolean value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, byte[] value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, int[] value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, long[] value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, float[] value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, double[] value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, boolean[] value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, String value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public void log(String identifier, String[] value) {
m_impl.log(m_prefix + identifier, value);
m_impl.log(withPrefix(identifier), value);
}
@Override
public <S> void log(String identifier, S value, Struct<S> struct) {
m_impl.log(m_prefix + identifier, value, struct);
m_impl.log(withPrefix(identifier), value, struct);
}
@Override
public <S> void log(String identifier, S[] value, Struct<S> struct) {
m_impl.log(m_prefix + identifier, value, struct);
m_impl.log(withPrefix(identifier), value, struct);
}
@Override
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
m_impl.log(m_prefix + identifier, value, proto);
}
}

View File

@@ -4,7 +4,9 @@
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import us.hebi.quickbuf.ProtoMessage;
/** Null backend implementation that logs nothing. */
public class NullBackend implements EpilogueBackend {
@@ -62,4 +64,8 @@ public class NullBackend implements EpilogueBackend {
@Override
public <S> void log(String identifier, S[] value, Struct<S> struct) {}
@Override
public <P, M extends ProtoMessage<M>> void log(
String identifier, P value, Protobuf<P, M> proto) {}
}

View File

@@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import edu.wpi.first.math.geometry.Rotation2d;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -185,4 +186,17 @@ class LazyBackendTest {
assertArrayEquals(
new byte[] {0x01, 0x00, 0x00, 0x00}, (byte[]) backend.getEntries().get(1).value());
}
@Test
void lazyProtobuf() {
var backend = new TestBackend();
var lazy = new LazyBackend(backend);
var rotation = Rotation2d.kZero;
lazy.log("rotation", rotation, Rotation2d.proto);
assertEquals(1, backend.getEntries().size());
var entry = backend.getEntries().get(0);
assertEquals("rotation", entry.identifier());
assertArrayEquals(new byte[] {9, 0, 0, 0, 0, 0, 0, 0, 0}, (byte[]) entry.value());
}
}

View File

@@ -0,0 +1,180 @@
// 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.epilogue.logging;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import org.junit.jupiter.api.Test;
class NestedBackendTest {
@Test
void prefixesAppliedAndNested() {
var root = new TestBackend();
var nested = new NestedBackend("/Robot", root);
nested.log("int", 1);
nested.log("string", "hello");
var arm = nested.getNested("arm");
arm.log("position", 2.0);
arm.log("enabled", true);
assertEquals(4, root.getEntries().size());
assertEquals("/Robot/int", root.getEntries().get(0).identifier());
assertEquals(1, root.getEntries().get(0).value());
assertEquals("/Robot/string", root.getEntries().get(1).identifier());
assertEquals("hello", root.getEntries().get(1).value());
assertEquals("/Robot/arm/position", root.getEntries().get(2).identifier());
assertEquals(2.0, root.getEntries().get(2).value());
assertEquals("/Robot/arm/enabled", root.getEntries().get(3).identifier());
assertEquals(true, root.getEntries().get(3).value());
}
@Test
void handlesTrailingSlashOnPrefix() {
var root = new TestBackend();
var a = new NestedBackend("/Robot", root);
var b = new NestedBackend("/Robot/", root);
a.log("x", 1);
b.log("y", 2);
assertEquals("/Robot/x", root.getEntries().get(0).identifier());
assertEquals("/Robot/y", root.getEntries().get(1).identifier());
}
@Test
void getNestedIsCached() {
var root = new TestBackend();
var nested = new NestedBackend("/Robot", root);
var arm1 = nested.getNested("arm");
var arm2 = nested.getNested("arm");
assertSame(arm1, arm2);
}
@Test
void usesPrefixedIdentifierCacheForSameField() {
var root = new TestBackend();
var nested = new NestedBackend("/Robot", root);
// Same field logged multiple times - identifier object should be the same (cached)
// We use assertSame to check that the references are identical
nested.log("x", 0);
nested.log("x", 1);
String id0 = root.getEntries().get(0).identifier();
String id1 = root.getEntries().get(1).identifier();
assertSame(
id0,
id1,
"Identifier %s (id: %d) was not reused (new id: %d)"
.formatted(id0, System.identityHashCode(id0), System.identityHashCode(id1)));
// Also verify through a nested backend path
var arm = nested.getNested("arm");
arm.log("position", 0.0);
arm.log("position", 1.0);
String id2 = root.getEntries().get(2).identifier();
String id3 = root.getEntries().get(3).identifier();
assertSame(
id2,
id3,
"Identifier %s (id: %d) was not reused (new id: %d)"
.formatted(id2, System.identityHashCode(id2), System.identityHashCode(id3)));
// Sanity check actual full values
assertEquals("/Robot/x", id0);
assertEquals("/Robot/arm/position", id2);
}
@Test
void logsAllOverloads() {
var root = new TestBackend();
var nested = new NestedBackend("/Robot", root);
// Scalars
nested.log("int", 1);
nested.log("long", 2L);
nested.log("float", 3.0f);
nested.log("double", 4.0);
nested.log("boolean", true);
nested.log("string", "hello");
// Arrays
nested.log("bytes", new byte[] {1, 2});
nested.log("ints", new int[] {3, 4});
nested.log("longs", new long[] {5L, 6L});
nested.log("floats", new float[] {7.0f, 8.0f});
nested.log("doubles", new double[] {9.0, 10.0});
nested.log("booleans", new boolean[] {true, false});
nested.log("strings", new String[] {"x", "y"});
// Structs
nested.log("customStruct", new CustomStruct(7), CustomStruct.struct);
nested.log(
"customStructs",
new CustomStruct[] {new CustomStruct(0), new CustomStruct(1)},
CustomStruct.struct);
var entries = root.getEntries();
int idx = 0;
// Scalars
assertEquals(new TestBackend.LogEntry<>("/Robot/int", 1), entries.get(idx++));
assertEquals(new TestBackend.LogEntry<>("/Robot/long", 2L), entries.get(idx++));
assertEquals(new TestBackend.LogEntry<>("/Robot/float", 3.0f), entries.get(idx++));
assertEquals(new TestBackend.LogEntry<>("/Robot/double", 4.0), entries.get(idx++));
assertEquals(new TestBackend.LogEntry<>("/Robot/boolean", true), entries.get(idx++));
assertEquals(new TestBackend.LogEntry<>("/Robot/string", "hello"), entries.get(idx++));
// Arrays
assertEquals("/Robot/bytes", entries.get(idx).identifier());
assertArrayEquals(new byte[] {1, 2}, (byte[]) entries.get(idx++).value());
assertEquals("/Robot/ints", entries.get(idx).identifier());
assertArrayEquals(new int[] {3, 4}, (int[]) entries.get(idx++).value());
assertEquals("/Robot/longs", entries.get(idx).identifier());
assertArrayEquals(new long[] {5L, 6L}, (long[]) entries.get(idx++).value());
assertEquals("/Robot/floats", entries.get(idx).identifier());
assertArrayEquals(new float[] {7.0f, 8.0f}, (float[]) entries.get(idx++).value());
assertEquals("/Robot/doubles", entries.get(idx).identifier());
assertArrayEquals(new double[] {9.0, 10.0}, (double[]) entries.get(idx++).value());
assertEquals("/Robot/booleans", entries.get(idx).identifier());
assertArrayEquals(new boolean[] {true, false}, (boolean[]) entries.get(idx++).value());
assertEquals("/Robot/strings", entries.get(idx).identifier());
assertArrayEquals(new String[] {"x", "y"}, (String[]) entries.get(idx++).value());
// Structs are serialized to bytes
assertEquals("/Robot/customStruct", entries.get(idx).identifier());
assertArrayEquals(new byte[] {0x07, 0x00, 0x00, 0x00}, (byte[]) entries.get(idx++).value());
assertEquals("/Robot/customStructs", entries.get(idx).identifier());
// two int32 values, little-endian
assertArrayEquals(
new byte[] {
0x00, 0x00, 0x00, 0x00, // 0 (first element)
0x01, 0x00, 0x00, 0x00, // 1 (second element)
0x00, 0x00, 0x00, 0x00, // 0 (empty space allocated by StructBuffer)
0x00, 0x00, 0x00, 0x00 // 0 (empty space allocated by StructBuffer)
},
(byte[]) entries.get(idx++).value());
// Ensure we covered all calls
assertEquals(idx, entries.size());
}
}

View File

@@ -4,12 +4,14 @@
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import edu.wpi.first.util.struct.StructBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import us.hebi.quickbuf.ProtoMessage;
@SuppressWarnings("PMD.TestClassWithoutTestCases") // This is not a test class!
public class TestBackend implements EpilogueBackend {
@@ -114,4 +116,12 @@ public class TestBackend implements EpilogueBackend {
m_entries.add(new LogEntry<>(identifier, serialized));
}
@Override
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
var msg = proto.createMessage();
proto.pack(msg, value);
var serialized = msg.toByteArray();
m_entries.add(new LogEntry<>(identifier, serialized));
}
}

View File

@@ -63,8 +63,8 @@ model {
artifact cppSourcesZip
artifactId = "${baseArtifactId}-cpp"
groupId artifactGroupId
version wpilibVersioning.version.get()
groupId = artifactGroupId
version = wpilibVersioning.version.get()
}
}
}

View File

@@ -86,7 +86,7 @@ model {
// Create the ZIP.
def task = project.tasks.create("copyGlassExecutable" + binary.targetPlatform.operatingSystem.name + binary.targetPlatform.architecture.name, Zip) {
description("Copies the Glass executable to the outputs directory.")
description = "Copies the Glass executable to the outputs directory."
destinationDirectory = outputsFolder
archiveBaseName = zipBaseName
@@ -175,7 +175,7 @@ model {
artifactId = baseArtifactId
groupId = artifactGroupId
version wpilibVersioning.version.get()
version = wpilibVersioning.version.get()
}
libglass(MavenPublication) {
libGlassTaskList.each { artifact it }
@@ -185,7 +185,7 @@ model {
artifactId = libBaseArtifactId
groupId = libArtifactGroupId
version wpilibVersioning.version.get()
version = wpilibVersioning.version.get()
}
libglassnt(MavenPublication) {
libGlassntTaskList.each { artifact it }
@@ -195,7 +195,7 @@ model {
artifactId = libntBaseArtifactId
groupId = libntArtifactGroupId
version wpilibVersioning.version.get()
version = wpilibVersioning.version.get()
}
}
}

View File

@@ -148,11 +148,14 @@ void glass::DisplayFMSReadOnly(FMSModel* model) {
ImGui::TextUnformatted("?");
}
}
wpi::SmallString<64> gameSpecificMessageBuf;
std::string_view gameSpecificMessage =
model->GetGameSpecificMessage(gameSpecificMessageBuf);
ImGui::Text("Game Specific: %s", exists ? gameSpecificMessage.data() : "?");
if (exists) {
wpi::SmallString<64> gsmBuf;
std::string_view gsm = model->GetGameSpecificMessage(gsmBuf);
ImGui::Text("Game Specific: %.*s", static_cast<int>(gsm.size()),
gsm.data());
} else {
ImGui::TextUnformatted("Game Specific: ?");
}
if (!exists) {
ImGui::PopStyleColor();

View File

@@ -4,11 +4,8 @@
#include "glass/other/PIDController.h"
#include <string>
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
using namespace glass;
@@ -34,8 +31,8 @@ void glass::DisplayPIDController(PIDControllerModel* m) {
[flag](const char* name, double* v,
std::function<void(double)> callback) {
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
if (ImGui::InputScalar(name, ImGuiDataType_Double, v, NULL, NULL,
"%.3f", flag)) {
if (ImGui::InputScalar(name, ImGuiDataType_Double, v, nullptr,
nullptr, "%.3f", flag)) {
callback(*v);
}
};

View File

@@ -4,11 +4,8 @@
#include "glass/other/ProfiledPIDController.h"
#include <string>
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
using namespace glass;
@@ -34,8 +31,8 @@ void glass::DisplayProfiledPIDController(ProfiledPIDControllerModel* m) {
[flag](const char* name, double* v,
std::function<void(double)> callback) {
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
if (ImGui::InputScalar(name, ImGuiDataType_Double, v, NULL, NULL,
"%.3f", flag)) {
if (ImGui::InputScalar(name, ImGuiDataType_Double, v, nullptr,
nullptr, "%.3f", flag)) {
callback(*v);
}
};

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -15,19 +15,19 @@ package edu.wpi.first.hal;
public class AnalogJNI extends JNIWrapper {
/**
* <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:58</i><br>
* enum values
* enum values.
*/
public interface AnalogTriggerType {
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:54</i> */
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:54</i>. */
int kInWindow = 0;
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:55</i> */
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:55</i>. */
int kState = 1;
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:56</i> */
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:56</i>. */
int kRisingPulse = 2;
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:57</i> */
/** <i>native declaration : AthenaJava\target\native\include\HAL\Analog.h:57</i>. */
int kFallingPulse = 3;
}

View File

@@ -35,10 +35,10 @@ public final class CANAPITypes {
kGyroSensor(4),
/** Accelerometer. */
kAccelerometer(5),
/** Ultrasonic sensor. */
kUltrasonicSensor(6),
/** Gear tooth sensor. */
kGearToothSensor(7),
/** Distance sensor. */
kDistanceSensor(6),
/** Encoder. */
kEncoder(7),
/** Power distribution. */
kPowerDistribution(8),
/** Pneumatics. */
@@ -49,11 +49,13 @@ public final class CANAPITypes {
kIOBreakout(11),
/** Servo Controller. */
kServoController(12),
/** Color Sensor. */
kColorSensor(13),
/** Firmware update. */
kFirmwareUpdate(31);
/** The device type ID. */
@SuppressWarnings("PMD.MemberName")
@SuppressWarnings("MemberName")
public final int id;
CANDeviceType(int id) {
@@ -105,10 +107,18 @@ public final class CANAPITypes {
/** AndyMark. */
kAndyMark(15),
/** Vivid-Hosting. */
kVividHosting(16);
kVividHosting(16),
/** Vertos Robotics. */
kVertosRobotics(17),
/** SWYFT Robotics. */
kSWYFTRobotics(18),
/** Lumyn Labs. */
kLumynLabs(19),
/** Brushland Labs. */
kBrushlandLabs(20);
/** The manufacturer ID. */
@SuppressWarnings("PMD.MemberName")
@SuppressWarnings("MemberName")
public final int id;
CANManufacturer(int id) {

View File

@@ -125,8 +125,8 @@ public class PowerDistributionFaults {
case 21 -> Channel21BreakerFault;
case 22 -> Channel22BreakerFault;
case 23 -> Channel23BreakerFault;
default -> throw new IndexOutOfBoundsException(
"Power distribution fault channel out of bounds!");
default ->
throw new IndexOutOfBoundsException("Power distribution fault channel out of bounds!");
};
}

View File

@@ -134,8 +134,8 @@ public class PowerDistributionStickyFaults {
case 21 -> Channel21BreakerFault;
case 22 -> Channel22BreakerFault;
case 23 -> Channel23BreakerFault;
default -> throw new IndexOutOfBoundsException(
"Power distribution fault channel out of bounds!");
default ->
throw new IndexOutOfBoundsException("Power distribution fault channel out of bounds!");
};
}

View File

@@ -32,15 +32,14 @@ public final class CANExceptionFactory {
case NIRioStatus.kRioStatusSuccess -> {
// Everything is ok... don't throw.
}
case ERR_CANSessionMux_InvalidBuffer,
NIRioStatus.kRIOStatusBufferInvalidSize -> throw new CANInvalidBufferException();
case ERR_CANSessionMux_MessageNotFound,
NIRioStatus.kRIOStatusOperationTimedOut -> throw new CANMessageNotFoundException();
case ERR_CANSessionMux_NotAllowed,
NIRioStatus.kRIOStatusFeatureNotSupported -> throw new CANMessageNotAllowedException(
"MessageID = " + messageID);
case ERR_CANSessionMux_NotInitialized,
NIRioStatus.kRIOStatusResourceNotInitialized -> throw new CANNotInitializedException();
case ERR_CANSessionMux_InvalidBuffer, NIRioStatus.kRIOStatusBufferInvalidSize ->
throw new CANInvalidBufferException();
case ERR_CANSessionMux_MessageNotFound, NIRioStatus.kRIOStatusOperationTimedOut ->
throw new CANMessageNotFoundException();
case ERR_CANSessionMux_NotAllowed, NIRioStatus.kRIOStatusFeatureNotSupported ->
throw new CANMessageNotAllowedException("MessageID = " + messageID);
case ERR_CANSessionMux_NotInitialized, NIRioStatus.kRIOStatusResourceNotInitialized ->
throw new CANNotInitializedException();
default -> throw new UncleanStatusException("Fatal status code detected: " + status);
}
}

View File

@@ -5,6 +5,7 @@
package edu.wpi.first.hal.simulation;
/** Interface for simulation buffer callbacks. */
@FunctionalInterface
public interface BufferCallback {
/**
* Simulation buffer callback function.

View File

@@ -4,6 +4,7 @@
package edu.wpi.first.hal.simulation;
@FunctionalInterface
public interface ConstBufferCallback {
void callback(String name, byte[] buffer, int count);
}

View File

@@ -6,6 +6,7 @@ package edu.wpi.first.hal.simulation;
import edu.wpi.first.hal.HALValue;
@FunctionalInterface
public interface NotifyCallback {
void callback(String name, HALValue value);

View File

@@ -4,6 +4,7 @@
package edu.wpi.first.hal.simulation;
@FunctionalInterface
public interface SpiReadAutoReceiveBufferCallback {
int callback(String name, int[] buffer, int numToRead);
}

View File

@@ -398,8 +398,9 @@ void HAL_SetCTREPCMOneShotDuration(HAL_CTREPCMHandle handle, int32_t index,
}
std::scoped_lock lock{pcm->lock};
pcm->oneShot.sol10MsPerUnit[index] = (std::min)(
static_cast<uint32_t>(durMs) / 10, static_cast<uint32_t>(0xFF));
pcm->oneShot.sol10MsPerUnit[index] =
(std::min)(static_cast<uint32_t>(durMs) / 10,
static_cast<uint32_t>(0xFF));
HAL_WriteCANPacketRepeating(pcm->canHandle, pcm->oneShot.sol10MsPerUnit, 8,
Control3, SendPeriod, status);
}

View File

@@ -32,10 +32,10 @@ HAL_ENUM(HAL_CANDeviceType) {
HAL_CAN_Dev_kGyroSensor = 4,
/// Accelerometer.
HAL_CAN_Dev_kAccelerometer = 5,
/// Ultrasonic sensor.
HAL_CAN_Dev_kUltrasonicSensor = 6,
/// Gear tooth sensor.
HAL_CAN_Dev_kGearToothSensor = 7,
/// Distance sensor.
HAL_CAN_Dev_kDistanceSensor = 6,
/// Encoder.
HAL_CAN_Dev_kEncoder = 7,
/// Power distribution.
HAL_CAN_Dev_kPowerDistribution = 8,
/// Pneumatics.
@@ -44,8 +44,10 @@ HAL_ENUM(HAL_CANDeviceType) {
HAL_CAN_Dev_kMiscellaneous = 10,
/// IO breakout.
HAL_CAN_Dev_kIOBreakout = 11,
// Servo controller.
/// Servo controller.
HAL_CAN_Dev_kServoController = 12,
/// Color Sensor.
HAL_CAN_Dev_ColorSensor = 13,
/// Firmware update.
HAL_CAN_Dev_kFirmwareUpdate = 31
};
@@ -89,6 +91,14 @@ HAL_ENUM(HAL_CANManufacturer) {
/// AndyMark.
HAL_CAN_Man_kAndyMark = 15,
/// Vivid-Hosting.
HAL_CAN_Man_kVividHosting = 16
HAL_CAN_Man_kVividHosting = 16,
/// Vertos Robotics.
HAL_CAN_Man_kVertosRobotics = 17,
/// SWYFT Robotics.
HAL_CAN_Man_kSWYFTRobotics = 18,
/// Lumyn Labs.
HAL_CAN_Man_kLumynLabs = 19,
/// Brushland Labs
HAL_CAN_Man_kBrushlandLabs = 20
};
/** @} */

View File

@@ -102,7 +102,7 @@ class SimCallbackRegistry : public impl::SimCallbackRegistryBase {
*/
#define HAL_SIMCALLBACKREGISTRY_DEFINE_NAME(NAME) \
static LLVM_ATTRIBUTE_ALWAYS_INLINE constexpr const char* \
Get##NAME##Name() { \
Get##NAME##Name() { \
return #NAME; \
}

View File

@@ -127,7 +127,7 @@ class SimDataValue final : public impl::SimDataValueBase<T, MakeValue> {
*/
#define HAL_SIMDATAVALUE_DEFINE_NAME(NAME) \
static LLVM_ATTRIBUTE_ALWAYS_INLINE constexpr const char* \
Get##NAME##Name() { \
Get##NAME##Name() { \
return #NAME; \
}

11
javacPlugin/BUILD.bazel Normal file
View File

@@ -0,0 +1,11 @@
load("@rules_java//java:defs.bzl", "java_plugin")
java_plugin(
name = "plugin",
srcs = glob(["src/main/java/**/*.java"]),
resources = glob(["src/main/resources/**"]),
visibility = ["//visibility:public"],
deps = [
"//wpiannotations",
],
)

17
javacPlugin/build.gradle Normal file
View File

@@ -0,0 +1,17 @@
ext {
useJava = true
useCpp = false
baseId = 'wpilibj-javac-plugin'
groupId = 'org.wpilib'
nativeName = ''
devMain = ''
}
apply from: "${rootDir}/shared/java/javacommon.gradle"
dependencies {
implementation project(':wpiannotations')
testImplementation 'com.google.testing.compile:compile-testing:+'
testImplementation project(':wpilibNewCommands')
}

View File

@@ -0,0 +1,224 @@
// 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.javacplugin;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import org.wpilib.annotation.NoDiscard;
/** Checks for usages of methods that require their return values to be used. */
public class ReturnValueUsedListener implements TaskListener {
private final JavacTask m_task;
private final Set<CompilationUnitTree> m_visitedCUs = new HashSet<>();
public ReturnValueUsedListener(JavacTask task) {
m_task = task;
}
@Override
public void finished(TaskEvent e) {
// We override `finished` instead of `started` because we want to run after the
// ANALYZE attribution phase has completed and assigned types to elements in the AST
// Track the visited CUs to avoid re-processing the same CU multiple times when we call
// `Trees.getElement()` on a tree path.
if (e.getKind() == TaskEvent.Kind.ANALYZE && m_visitedCUs.add(e.getCompilationUnit())) {
e.getCompilationUnit().accept(new Scanner(e.getCompilationUnit()), null);
}
}
private final class Scanner extends TreeScanner<Void, Void> {
private final CompilationUnitTree m_root;
private final Trees m_trees;
Scanner(CompilationUnitTree compilationUnit) {
m_root = compilationUnit;
m_trees = Trees.instance(m_task);
}
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
checkIgnoredExpression(node);
return super.visitMethodInvocation(node, unused);
}
@Override
public Void visitNewClass(NewClassTree node, Void unused) {
checkIgnoredExpression(node);
return super.visitNewClass(node, unused);
}
/**
* Common logic for both method invocations and constructor calls when they appear as
* stand-alone expression statements (i.e., their result is ignored).
*/
private void checkIgnoredExpression(Tree node) {
var path = m_trees.getPath(m_root, node);
// Walk the tree upwards to see if the node is directly or indirectly annotated with
// @SuppressWarnings("NoDiscard") or @SuppressWarnings("all"). If so, then we ignore any
// @NoDiscard messages for this node
for (var currentPath = path; currentPath != null; currentPath = currentPath.getParentPath()) {
var element = m_trees.getElement(currentPath);
if (element == null) {
continue;
}
if (element.getAnnotation(SuppressWarnings.class) != null) {
String[] suppressions = element.getAnnotation(SuppressWarnings.class).value();
for (String suppression : suppressions) {
if ("NoDiscard".equals(suppression) || "all".equals(suppression)) {
return;
}
}
}
}
var parentPath = (path == null) ? null : path.getParentPath();
if (parentPath == null || parentPath.getLeaf().getKind() != Tree.Kind.EXPRESSION_STATEMENT) {
// If the parent node is an expression statement, then the value is ignored.
// Otherwise, the value is used and we can ignore this site.
return;
}
// Resolve the static type of the expression
TypeMirror type = getType(node);
if (type == null || type.getKind() == TypeKind.VOID) {
// Skip void (e.g., void-returning methods)
return;
}
// Check @NoDiscard on the invoked executable (method or constructor)
var invoked = getInvokedExecutable(node);
if (invoked != null) {
List<String> messages = getNoDiscardMessages(invoked);
for (String msg : messages) {
m_trees.printMessage(Diagnostic.Kind.ERROR, msg, node, m_root);
}
}
}
private TypeMirror getType(Tree node) {
var path = m_trees.getPath(m_root, node);
if (path == null) {
return null;
}
// Requires running after ANALYZE attribution has completed for this CU.
return m_trees.getTypeMirror(path);
}
private ExecutableElement getInvokedExecutable(Tree node) {
var path = m_trees.getPath(m_root, node);
if (path == null) {
return null;
}
var el = m_trees.getElement(path);
return (el instanceof ExecutableElement ee) ? ee : null;
}
/**
* Collects all @NoDiscard messages applicable to the given executable: - The method/constructor
* itself (if annotated) - The return type (if declared) including its superclasses and all
* implemented interfaces Returns formatted diagnostics messages ready to print.
*/
private List<String> getNoDiscardMessages(ExecutableElement method) {
List<String> messages = new ArrayList<>();
// 1) Method-level @NoDiscard
var methodNoDiscard = method.getAnnotation(NoDiscard.class);
if (methodNoDiscard != null) {
String msg = methodNoDiscard.value();
if (msg.isEmpty()) {
messages.add("Result of @NoDiscard method is ignored");
} else {
messages.add(msg);
}
}
// 2) Type-level @NoDiscard (classes + interfaces recursively)
TypeElement targetType = null;
if (method.getKind() == ElementKind.CONSTRUCTOR) {
// For constructors, the "return type" is the enclosing type
var enclosing = method.getEnclosingElement();
if (enclosing instanceof TypeElement te) {
targetType = te;
}
} else {
var returnType = method.getReturnType();
if (returnType instanceof DeclaredType dt && dt.asElement() instanceof TypeElement te) {
targetType = te;
}
}
if (targetType != null) {
Set<TypeElement> seen = new HashSet<>();
collectNoDiscardMessagesFromTypeHierarchy(targetType, seen, messages);
}
return messages;
}
/**
* Searches for @NoDiscard on the provided type element, its superclasses, and all implemented
* interfaces (recursively). Appends formatted messages to the provided list for every match.
*
* @param type The type element to search
* @param seen A set of type elements that have already been searched
* @param out The list to append messages to
*/
private void collectNoDiscardMessagesFromTypeHierarchy(
TypeElement type, Set<TypeElement> seen, List<String> out) {
if (type == null || !seen.add(type)) {
return;
}
// Check this type directly
var typeNoDiscard = type.getAnnotation(NoDiscard.class);
if (typeNoDiscard != null) {
String message = typeNoDiscard.value();
if (message.isEmpty()) {
out.add(
"Result of method returning @NoDiscard type %s is ignored"
.formatted(type.getQualifiedName()));
} else {
out.add(message);
}
}
// Check superclass chain
var superMirror = type.getSuperclass();
if (superMirror != null && superMirror.getKind() != TypeKind.NONE) {
var superEl = m_task.getTypes().asElement(superMirror);
if (superEl instanceof TypeElement ste) {
collectNoDiscardMessagesFromTypeHierarchy(ste, seen, out);
}
}
// Check all implemented interfaces (recursively)
for (var iface : type.getInterfaces()) {
var ifaceEl = m_task.getTypes().asElement(iface);
if (ifaceEl instanceof TypeElement ite) {
collectNoDiscardMessagesFromTypeHierarchy(ite, seen, out);
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
// 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.javacplugin;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
/**
* A javac compiler plugin that adds compiler warnings for incorrect usage of WPILib types. Also
* supports WPILib's custom annotations like @NoDiscard.
*/
public class WPILibJavacPlugin implements Plugin {
@Override
public String getName() {
return "WPILib";
}
@Override
public void init(JavacTask task, String... args) {
task.addTaskListener(new ReturnValueUsedListener(task));
}
@Override
public boolean autoStart() {
// autoStart means we don't need to manually pass -Xplugin:WPILib to the javac compiler args
// for the plugin to run
return true;
}
}

View File

@@ -0,0 +1 @@
org.wpilib.javacplugin.WPILibJavacPlugin

View File

@@ -0,0 +1,13 @@
// 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.javacplugin;
import java.util.List;
public class CompileTestOptions {
public static final int kJavaVersion = 17;
public static final List<Object> kJavaVersionOptions =
List.of("-source", kJavaVersion, "-target", kJavaVersion);
}

View File

@@ -0,0 +1,638 @@
// 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.javacplugin;
import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.wpilib.javacplugin.CompileTestOptions.kJavaVersionOptions;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import org.junit.jupiter.api.Test;
class ReturnValueUsedListenerTest {
@Test
void nodiscardReturnValueIsUsed() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
class Example {
@NoDiscard
int getI() { return 0; }
void usage() {
int i = getI();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void nodiscardReturnValueUnused() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
class Example {
@NoDiscard
int getI() { return 0; }
void usage() {
getI();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals("Result of @NoDiscard method is ignored", error.getMessage(null));
}
@Test
void nodiscardOnClass() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@NoDiscard
class Example {
Example getExample() { return new Example(); }
void usage() {
getExample();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals(
"Result of method returning @NoDiscard type frc.robot.Example is ignored",
error.getMessage(null));
}
@Test
void nodiscardOnClassCustomMessage() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@NoDiscard("Custom message")
class Example {
Example getExample() { return new Example(); }
void usage() {
getExample();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals("Custom message", error.getMessage(null));
}
@Test
void nodiscardOnClassAndMethod() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@NoDiscard
class Example {
@NoDiscard
Example getExample() { return new Example(); }
void usage() {
getExample();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(2, compilation.errors().size());
var error1 = compilation.errors().get(0);
var error2 = compilation.errors().get(1);
assertEquals("Result of @NoDiscard method is ignored", error1.getMessage(null));
assertEquals(
"Result of method returning @NoDiscard type frc.robot.Example is ignored",
error2.getMessage(null));
}
@Test
void nodiscardOnInheritedClass() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@NoDiscard("Objects of type `Base` must be used")
abstract class Base { }
class Example extends Base {
Example getExample() { return new Example(); }
void usage() {
getExample();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals("Objects of type `Base` must be used", error.getMessage(null));
}
@Test
void nodiscardOnSingleInterface() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@NoDiscard("Objects implementing `I` must be used")
interface I { }
class Example implements I {
Example getExample() { return new Example(); }
void usage() {
getExample();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals("Objects implementing `I` must be used", error.getMessage(null));
}
@Test
void nodiscardOnMultipleInterfaces() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@NoDiscard("Objects implementing `I` must be used")
interface I { }
@NoDiscard("Objects implementing `I2` must be used")
interface I2 { }
class Example implements I, I2 {
Example getExample() { return new Example(); }
void usage() {
getExample();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(2, compilation.errors().size());
var error1 = compilation.errors().get(0);
var error2 = compilation.errors().get(1);
assertEquals("Objects implementing `I` must be used", error1.getMessage(null));
assertEquals("Objects implementing `I2` must be used", error2.getMessage(null));
}
@Test
void nodiscardCustomMessage() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
class Example {
@NoDiscard("Custom message")
int getI() { return 0; }
void usage() {
getI();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals("Custom message", error.getMessage(null));
}
@Test
void nodiscardMessageEmptyString() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
class Example {
@NoDiscard("")
int getI() { return 0; }
void usage() {
getI();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals("Result of @NoDiscard method is ignored", error.getMessage(null));
}
@Test
void nodiscardOnVoidMethod() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
class Example {
@NoDiscard
void voidMethod() { }
void usage() {
voidMethod();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void suppressWarningsOnNoDiscardMethod() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
class Example {
@NoDiscard
Object get() { return null; }
@SuppressWarnings("NoDiscard")
void usage() {
get();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void suppressWarningsAllOnNoDiscardMethod() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
class Example {
@NoDiscard
Object get() { return null; }
@SuppressWarnings("all")
void usage() {
get();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void suppressWarningsOnNoDiscardClass() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@SuppressWarnings("NoDiscard")
class Example {
@NoDiscard
Object get() { return null; }
void usage() {
get();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void suppressWarningsAllOnNoDiscardClass() {
String source =
"""
package frc.robot;
import org.wpilib.annotation.NoDiscard;
@SuppressWarnings("all")
class Example {
@NoDiscard
Object get() { return null; }
void usage() {
get();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void commandsv2CommandFactoryResultIsAssigned() {
String source =
"""
package frc.robot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.Commands;
import org.wpilib.annotation.NoDiscard;
class Example {
Command getCommand() {
return Commands.print("");
}
void usage() {
Command theCommand = getCommand();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void commandsv2CommandFactoryResultIsPassed() {
String source =
"""
package frc.robot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.Commands;
import org.wpilib.annotation.NoDiscard;
class Example {
Command getCommand() {
return Commands.print("");
}
void usage() {
System.out.println(getCommand());
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void commandsv2CommandFactoryResultIsChainedAndUsed() {
String source =
"""
package frc.robot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.Commands;
import org.wpilib.annotation.NoDiscard;
class Example {
Command getCommand() {
return Commands.print("");
}
void usage() {
Command theCommand = getCommand().withName("The name");
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).succeededWithoutWarnings();
}
@Test
void commandsv2CommandFactoryResultNotUsed() {
String source =
"""
package frc.robot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.Commands;
import org.wpilib.annotation.NoDiscard;
class Example {
Command getCommand() {
return Commands.print("");
}
void usage() {
getCommand();
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals(
"Commands must be used! Did you mean to bind it to a trigger?", error.getMessage(null));
}
@Test
void commandsv2CommandFactoryResultIsChainedAndNotUsed() {
String source =
"""
package frc.robot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.Commands;
import org.wpilib.annotation.NoDiscard;
class Example {
Command getCommand() {
return Commands.print("");
}
void usage() {
getCommand().withName("The name");
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals(
"Commands must be used! Did you mean to bind it to a trigger?", error.getMessage(null));
}
@Test
void commandsv2NewCommandInstanceNotUsed() {
String source =
"""
package frc.robot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.Commands;
import edu.wpi.first.wpilibj2.command.WaitCommand;
import org.wpilib.annotation.NoDiscard;
class Example {
void usage() {
new WaitCommand(1);
}
}
""";
Compilation compilation =
javac()
.withOptions(kJavaVersionOptions)
.compile(JavaFileObjects.forSourceString("frc.robot.Example", source));
assertThat(compilation).failed();
assertEquals(1, compilation.errors().size());
var error = compilation.errors().get(0);
assertEquals(
"Commands must be used! Did you mean to bind it to a trigger?", error.getMessage(null));
}
}

View File

@@ -1,6 +1,5 @@
import org.gradle.nativeplatform.toolchain.internal.msvcpp.VisualStudioLocator
import org.gradle.internal.os.OperatingSystem
import org.gradle.util.VersionNumber
plugins {
id 'cpp'
@@ -69,8 +68,8 @@ if (OperatingSystem.current().isWindows()) {
artifact x64ZipTask
artifactId = "${baseArtifactId}"
groupId artifactGroupId
version wpilibVersioning.version.get()
groupId = artifactGroupId
version = wpilibVersioning.version.get()
}
}
}

View File

@@ -44,6 +44,7 @@ Checks:
-clang-diagnostic-#warnings,
-clang-diagnostic-pedantic,
clang-analyzer-*,
-clang-analyzer-optin.cplusplus.UninitializedObject,
cppcoreguidelines-slicing,
google-build-namespaces,
google-explicit-constructor,

View File

@@ -11,20 +11,17 @@
int main() {
auto inst = nt::GetDefaultInstance();
nt::AddLogger(
inst,
[](const nt::LogMessage& msg) {
std::fputs(msg.message.c_str(), stderr);
std::fputc('\n', stderr);
},
0, UINT_MAX);
nt::StartClient(inst, "127.0.0.1", 10000);
nt::AddLogger(inst, 0, UINT_MAX, [](const nt::Event& event) {
std::fputs(event.GetLogMessage()->message.c_str(), stderr);
std::fputc('\n', stderr);
});
nt::StartClient4(inst, "127.0.0.1");
std::this_thread::sleep_for(std::chrono::seconds(2));
auto foo = nt::GetEntry(inst, "/foo");
auto foo_val = nt::GetEntryValue(foo);
if (foo_val && foo_val->IsDouble()) {
std::printf("Got foo: %g\n", foo_val->GetDouble());
if (foo_val && foo_val.IsDouble()) {
std::printf("Got foo: %g\n", foo_val.GetDouble());
}
auto bar = nt::GetEntry(inst, "/bar");

View File

@@ -11,14 +11,11 @@
int main() {
auto inst = nt::GetDefaultInstance();
nt::AddLogger(
inst,
[](const nt::LogMessage& msg) {
std::fputs(msg.message.c_str(), stderr);
std::fputc('\n', stderr);
},
0, UINT_MAX);
nt::StartServer(inst, "persistent.ini", "", 10000);
nt::AddLogger(inst, 0, UINT_MAX, [](const nt::Event& event) {
std::fputs(event.GetLogMessage()->message.c_str(), stderr);
std::fputc('\n', stderr);
});
nt::StartServer(inst, "persistent.ini", "", 10000, 10001);
std::this_thread::sleep_for(std::chrono::seconds(1));
auto foo = nt::GetEntry(inst, "/foo");

View File

@@ -9,7 +9,6 @@ package edu.wpi.first.networktables;
import java.nio.ByteBuffer;
{% endif %}
/** NetworkTables {{ TypeName }} implementation. */
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
final class {{ TypeName }}EntryImpl extends EntryBase implements {{ TypeName }}Entry {
/**
* Constructor.

View File

@@ -9,7 +9,6 @@ package edu.wpi.first.networktables;
import java.util.function.Supplier;
/** NetworkTables generic subscriber. */
@SuppressWarnings("PMD.MissingOverride")
public interface GenericSubscriber extends Subscriber, Supplier<NetworkTableValue> {
/**
* Get the corresponding topic.

View File

@@ -9,7 +9,7 @@ package edu.wpi.first.networktables;
import java.util.Objects;
/** A network table entry value. */
@SuppressWarnings({"UnnecessaryParentheses", "PMD.MethodReturnsInternalArray"})
@SuppressWarnings("UnnecessaryParentheses")
public final class NetworkTableValue {
NetworkTableValue(NetworkTableType type, Object value, long time, long serverTime) {
m_type = type;
@@ -176,7 +176,6 @@ public final class NetworkTableValue {
return out;
}
@SuppressWarnings("PMD.AvoidArrayLoops")
static double[] toNativeDoubleArray(Number[] arr) {
double[] out = new double[arr.length];
for (int i = 0; i < arr.length; i++) {
@@ -185,7 +184,6 @@ public final class NetworkTableValue {
return out;
}
@SuppressWarnings("PMD.AvoidArrayLoops")
static long[] toNativeIntegerArray(Number[] arr) {
long[] out = new long[arr.length];
for (int i = 0; i < arr.length; i++) {
@@ -194,7 +192,6 @@ public final class NetworkTableValue {
return out;
}
@SuppressWarnings("PMD.AvoidArrayLoops")
static float[] toNativeFloatArray(Number[] arr) {
float[] out = new float[arr.length];
for (int i = 0; i < arr.length; i++) {

View File

@@ -9,7 +9,6 @@ package edu.wpi.first.networktables;
import {{ java.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
/** NetworkTables {{ TypeName }} subscriber. */
@SuppressWarnings("PMD.MissingOverride")
public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
/**
* Get the corresponding topic.

View File

@@ -7,7 +7,6 @@
package edu.wpi.first.networktables;
/** NetworkTables timestamped {{ TypeName }}. */
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
public final class Timestamped{{ TypeName }} {
/**
* Create a timestamped value.

View File

@@ -7,7 +7,6 @@
package edu.wpi.first.networktables;
/** NetworkTables BooleanArray implementation. */
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
final class BooleanArrayEntryImpl extends EntryBase implements BooleanArrayEntry {
/**
* Constructor.

View File

@@ -9,7 +9,6 @@ package edu.wpi.first.networktables;
import java.util.function.Supplier;
/** NetworkTables BooleanArray subscriber. */
@SuppressWarnings("PMD.MissingOverride")
public interface BooleanArraySubscriber extends Subscriber, Supplier<boolean[]> {
/**
* Get the corresponding topic.

Some files were not shown because too many files have changed in this diff Show More