Compare commits

...

84 Commits

Author SHA1 Message Date
Peter Johnson
f1a82828fe [wpiutil] Add DataLog and DataLogManager Stop() (#5860)
Restarting a stopped log results in creating a new log file with fresh copies of the same start records and schema data records.

Also check to see if the file has been deleted or if the log file exceeds 1.8 GB, and start a new one.
2023-11-03 20:34:43 -07:00
Ryan Blue
2a04e12c6f [apriltag] AprilTagFieldLayout: Add accessors for origin and field dimensions (#5869) 2023-11-03 20:24:23 -07:00
Ryan Blue
33e0089afb Cleanup usages of std::function<void(void)> (#5864) 2023-11-03 18:22:47 -07:00
Ryan Blue
d06fa633d5 [build] Fix protobuf generation when building with make (#5867) 2023-11-03 18:22:34 -07:00
Ryan Blue
049732afb8 [cscore] Make camera connection logging clearer (#5866) 2023-11-03 16:03:23 -07:00
Bryce Roethel
87f7c19f90 [wpimath] Make InterpolatingDoubleTreeMap constructor public (#5865) 2023-11-03 15:34:14 -07:00
Tyler Veness
6b53ef47cf [wpimath] Don't recreate TrapezoidProfile in ProfiledPIDController calculate() (#5863) 2023-11-03 15:21:58 -07:00
shueja-personal
8a3a268ae6 [commands] Add finallyDo with zero-arg lambda (#5862) 2023-11-03 15:21:21 -07:00
Tyler Veness
1c35d42cd0 [wpilib] Pop diagnostic for deprecated function use (#5859) 2023-11-03 08:34:08 -07:00
narmstro2020
ddc8db6c26 [wpimath] Add feedforward constant constructor to ElevatorSim (#5823)
Adds a subclass of ElevatorSim that uses kG, kV, and kA from sysId to simulate an Elevator.
2023-11-02 09:10:57 -07:00
Tyler Veness
c6aff2c431 [upstream_utils] Update to LLVM 17.0.4 (#5855) 2023-11-01 16:45:32 -07:00
Peter Johnson
a9c5b18a39 [build] Update OpenCV to 2024-4.8.0-2 (#5854)
This fixes rpath on cross-build targets to properly include $ORIGIN.
2023-11-01 09:45:40 -07:00
Kevin-OConnor
9540b6922d [hal] Add CAN IDs for AndyMark and Vivid Hosting (#5852) 2023-10-31 15:59:42 -07:00
Peter Johnson
83a7d33c47 [glass] Improve display of protobuf/struct type strings (#5850) 2023-10-30 20:29:29 -07:00
Ryan Blue
a4a8ad9c75 [commands] Make Java SelectCommand generic (#5849)
This allows users to use any type of map and selector without needing to explicitly match
Map<Object, Command>
2023-10-30 11:09:14 -07:00
Gold856
9eecf2a456 [build] Add CMake option to build Java sources jars (#5768) 2023-10-30 09:57:28 -07:00
David Baucum
9536a311cb [wpilib] Add support for the PS5 DualSense controller (#5257)
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2023-10-30 08:25:16 -07:00
Dustin Spicuzza
8d5e6737fc [wpilibc] SolenoidSim: Add virtual destructor (#5848) 2023-10-30 08:24:11 -07:00
Peter Johnson
07e13d60a2 [ntcore] Fix write_impl (#5847)
The previous fix didn't handle all cases correctly. Instead, add a new
function to raw_ostream (SetNumBytesInBuffer) to allow always using the
full buffer size, and revamp write_impl to more cleanly handle all
cases.
2023-10-30 08:23:33 -07:00
Peter Johnson
1713386869 [wpiutil] ProtobufMessageDatabase: Fix out-of-order Add() rebuild (#5845) 2023-10-29 16:50:01 -07:00
Peter Johnson
35472f5fc9 [ntcore] Fix a use-after-free in client close (#5844) 2023-10-29 16:49:45 -07:00
Peter Johnson
ed168b522c [ntcore] Disable buf pool when asan is enabled (#5843)
This helps asan catch more errors.
2023-10-29 16:49:31 -07:00
Peter Johnson
3e7ba2cc6f [wpinet] WebSocket: Fix write behavior (#5841)
On Windows, TryWrite will always return 0 if there is a Write in progress. The previous behavior for SendFrames and SendControl just used a normal Write, which caused issues with code that combined these with TrySendFrames. Instead, have SendFrames and SendControl also use TryWrite under the hood if possible, and create write requests if not. The implementation preserves the priority of SendControl against an existing write request with multiple frames.
2023-10-29 16:48:25 -07:00
Gold856
80c47da237 [sim] Disable the robot program when DS disconnects (#5818) 2023-10-28 10:10:23 -07:00
sciencewhiz
abe1cec90c [wpilib] Update Usage Reporting ResourceType from NI Libraries (#5842)
Disable PS4 controller reporting until added by NI
2023-10-27 20:21:46 -07:00
Oliver
cdf981abba [glass] Fix position of data type in NT view (#5840) 2023-10-27 17:41:42 -07:00
Thad House
04dcd80adb [build] Publish unit tests for examples (#5838) 2023-10-27 16:57:38 -07:00
Jordan McMichael
49920234ac [build] Fix checkstyle rules to allow Windows paths (#5839) 2023-10-27 16:56:58 -07:00
Ryan Blue
366b715942 [wpilib] Fix SendableChooser test (#5835)
Using unique NT keys for each test seems to resolve the failure on Linux. Changed Java as well, for completeness.
2023-10-26 20:47:04 -07:00
Ryan Blue
3ba501f947 [commands] Java: Fix CommandXboxController.leftTrigger() parameter order (#5831) 2023-10-26 19:18:36 -07:00
Tyler Veness
ec569a58ef [wpimath] Make KalmanTypeFilter interface public (#5830) 2023-10-26 19:18:02 -07:00
Peter Johnson
b91317fd36 [wpiutil] DataLog.addSchema(): Don't add into a set view (#5829) 2023-10-26 19:17:47 -07:00
Peter Johnson
2ab4fcbc24 [wpiutil] ProtobufMessageDatabase: Clear messages first (#5827)
The message destructor appears to call something on the descriptor.
2023-10-26 19:17:29 -07:00
Tyler Veness
98c14f1692 [wpimath] Add EKF/UKF u-y-R correct overload (#5832)
Also clean up comments on other overloads and fix a typo.
2023-10-26 19:17:15 -07:00
Ryan Blue
60bcdeded9 [ci] Disable java in sanitizer builds (#5833) 2023-10-26 19:16:58 -07:00
Ryan Blue
c87f8fd538 [commands] Add DeferredCommand (#5566)
Allows commands to be constructed at runtime without proxying.
2023-10-26 19:16:33 -07:00
Ryan Blue
ad80eb3a0b [ci] Update actions for comment-command (#5824) 2023-10-25 14:33:22 -07:00
Peter Johnson
c7d6ad5a0b [ntcore] WebSocketConnection: Use weak capture (#5822)
Fixes a potential use-after-free on connection close.
2023-10-25 10:22:43 -07:00
Zhiquan Yeo
8a8e220792 [simgui] Add 'Invalid' option for AllianceStation (#5820) 2023-10-24 11:51:39 -07:00
Gold856
cfc6a47f76 [sim] DS plugin: Fix off-by-one error when setting alliance station (#5819) 2023-10-24 09:15:40 -07:00
Peter Johnson
8efa586ace [ntcore] Don't check type string on publishing an entry (#5816)
Only check the raw type value.
2023-10-24 00:12:02 -07:00
Peter Johnson
23ea188e60 [glass] Add protobuf decode error log message (#5812) 2023-10-23 23:36:23 -07:00
Ryan Blue
928e87b4f4 [build] Add combined test meta-task (#5813) 2023-10-23 23:35:44 -07:00
Benjamin Hall
63ef585d4b [wpiutil] Fix compilation of MathExtras.h on Windows with /sdl (#5809)
Fix copied from the LLVM main branch.
2023-10-23 21:35:13 -07:00
Ryan Blue
b03a7668f9 [build] Windows CMake/vcpkg fixes (#5807)
- Add builtin registry baseline (fixes building locally with msvc builtin vcpkg)
- Add protobuf as an explicit dependency (previously was installed as a dependency of opencv)
- In windows CI, checkout repository before running vcpkg (silences warning that vcpkg.json was not found)
2023-10-23 21:33:28 -07:00
Thad House
3f08bcde54 [hal] Fix HAL AllianceStation on rio (#5811) 2023-10-23 21:32:21 -07:00
Peter Johnson
196d963dc4 [ntcore] Fix off-by-one error in stream write (#5810) 2023-10-23 21:31:36 -07:00
sciencewhiz
f4cbcbc984 Fix typos (NFC) (#5804) 2023-10-23 09:15:58 -07:00
Thad House
ec0f7fefb0 [myrobot] Update the myRobot JRE (#5805) 2023-10-23 09:14:50 -07:00
Jonah
3d618bdbfd [wpiutil] Fix Java struct array unpacking (#5801) 2023-10-21 20:13:50 -07:00
Peter Johnson
1fa7445667 [ntcore] Check for valid client in incoming text and binary (#5799) 2023-10-20 23:25:05 -07:00
Ryan Blue
269b9647da [ci] Update JDK for combine step (#5794)
Also frees disk space from the combiner step.
2023-10-20 18:18:04 -07:00
Tyler Veness
bee32f080e [docs] Add wpiunits to JavaDocs (#5793)
Co-authored-by: Sam Carlberg <sam.carlberg@gmail.com>
2023-10-20 18:14:14 -07:00
Peter Johnson
25dad5a531 [wpinet] TCPConnector_parallel: Don't use thread_local (#5791)
Thread_local causes issues with LabVIEW, which makes calls on a thread
pool.
2023-10-20 16:45:07 -07:00
Tyler Veness
4a93581f1a [build] cmake: use default library type for libglassnt, libglass, wpigui, and imgui (#5797)
On Windows, imgui and wpigui need to be static.  On Mac, they must be dynamic.
2023-10-20 16:44:26 -07:00
Ryan Blue
abb2857e03 [wpilib] Counter: Fix default distance per pulse, add distance and rate to C++ (#5796)
- Default distance per pulse in java was 0; 1 is a more reasonable default
- C++ was missing this functionality
2023-10-19 23:42:53 -07:00
Peter Johnson
b14a61e1c0 [readme] Add link to QuickBuffers release page (#5795) 2023-10-19 22:07:40 -07:00
Peter Johnson
cf54d9ccb7 [wpiutil, ntcore] Add structured data support (#5391)
This adds support for two serialization formats for complex data types:

- Protobuf for complex objects with variable length internals that need forward and backward wire compatibility (lower speed, more flexible)
- Raw struct (ByteBuffer-style) for fixed-length objects (higher speed, less flexible)

Deserialization can be done either by creating a new object (for immutable objects) or overwriting the contents of an existing object (for mutable objects).

Implementing classes should provide inner classes that implement the Protobuf or Struct interface (in Java) or specialize the wpi::Protobuf or wpi::Struct struct (in C++). It is possible for classes to implement both. If the class itself does not implement serialization, it's possible for third parties/users to provide an implementation instead.

Uses the Google protobuf implementation for C++ and the QuickBuffers alternative protobuf implementation for Java.
2023-10-19 21:41:47 -07:00
Jordan McMichael
ecb7cfa9ef [wpimath] Add Exponential motion profile (#5720) 2023-10-19 17:26:32 -07:00
Peter Johnson
7c6fe56cf2 [ntcore] Fix crash on disconnect (#5788)
The object was being destroyed due to the error handling path.
Instead, defer to the next loop cycle using a one-shot timer.

Properly handle error return values from Send functions.

Fix UB in accessing one past the end of a vector.
2023-10-19 14:36:22 -07:00
Peter Johnson
85147bf69e [wpinet] WebSocketSerializer: Fix UB (#5787) 2023-10-19 00:14:23 -07:00
Peter Johnson
244163acad [wpinet] uv::Stream::TryWrite(): Return 0 on EAGAIN (#5784)
EAGAIN is a normal return value, but we want to just map it to 0 rather than handling it as an error.
2023-10-19 00:12:34 -07:00
Gold856
820728503d [hal] Remove extra semicolon in RoboRioData (#5786) 2023-10-18 23:12:05 -07:00
Tyler Veness
45f307d87e [upstream_utils] Upgrade to LLVM 17.0.3 (#5785) 2023-10-18 19:18:35 -07:00
Peter Johnson
4ce4d63efc [wpilibj] Fix RobotBase.isSimulation() (#5783)
#3534 changed it from returning !isReal() to checking against a numeric value, but #3565 (merged earlier) had changed the value to an enum.
2023-10-18 14:15:35 -07:00
Ryan Blue
579007ceb3 [commands] Add requirements parameter to Commands.idle() (#5774) 2023-10-17 19:52:48 -07:00
Tyler Veness
3f3a169149 [wpilib] Make physics sim setState() functions public (#5779) 2023-10-17 19:52:02 -07:00
Tyler Veness
7501e4ac88 [wpilib] Close sim device in ADIS IMUs (#5776)
Fixes #5775.
2023-10-17 19:51:35 -07:00
Tyler Veness
99630d2e78 [wpimath] Upgrade to EJML 0.43.1 (#5778)
https://github.com/lessthanoptimal/ejml/blob/v0.43.1/change.txt
2023-10-17 19:51:17 -07:00
Tyler Veness
02cbbc997d [wpimath] Make Vector-Vector binary operators return Vector (#5772)
Fixes #5741.
2023-10-17 16:44:30 -07:00
Tyler Veness
ed93889e17 [examples] Fix typo in TimesliceRobot example name (#5773) 2023-10-17 16:44:18 -07:00
Tyler Veness
da70e4c262 [docs] Add jinja2 to CMake prerequisites (#5771)
Fixes #5769.
2023-10-17 11:05:34 -07:00
Brayden Zee
e814595ea7 [wpimath] Add ChassisSpeeds.fromRobotRelativeSpeeds() (#5744) 2023-10-17 10:13:04 -07:00
narmstro2020
f98c943445 [wpimath] LinearSystemId: Add DCMotorSystem overload (#5770)
The goal of this addition is to allow LinearSystemId.createDCMotorSystem to use kV and KA instead of the moment of inertia, DCMotor object, and gearing.
2023-10-17 10:11:34 -07:00
Peter Johnson
b3eb64b0f7 [wpiutil] ct_string: Use inline namespace for literals (#5767)
This avoids a conflict with json.
2023-10-15 07:53:08 -07:00
Peter Johnson
7d9ba256c2 Revert "[build] Add CMake option to build Java source jars (#5756)" (#5766)
This reverts commit 1c724884ca.

This commit broke local builds on the second run of cmake configure.
2023-10-14 23:53:27 -07:00
Tyler Veness
1f6492e3d8 [sysid] Update JSON library usage (#5765) 2023-10-14 23:52:50 -07:00
Peter Johnson
638f04f626 [wpiutil] Add protobuf to thirdparty sources (#5746) 2023-10-14 21:54:17 -07:00
PJ Reiniger
210255bfff [wpiutil] Update json to 3.11.2 (#5680) 2023-10-14 21:53:56 -07:00
Jordan McMichael
896772c750 [wpimath] Add DCMotor functions for Kraken X60 and Neo Vortex (#5759) 2023-10-14 21:52:01 -07:00
Tyler Veness
fd427f6c82 [wpimath] Fix hardcoded module count in SwerveDriveKinematics.resetHeading() (#5762)
Fixes #5761.
2023-10-14 21:51:15 -07:00
Anit Mangal
c0b4c6cce6 [wpimath] Add overloads for Transform2d and Transform3d (#5757)
Adds overloads for Transform2d() constructor to accept x, y, and heading and for Transform3d() to accept x, y, z and rotation as a shorthand for the normal constructors.
2023-10-12 23:21:39 -07:00
kully
9a0aafd8ab [examples] Make swerve examples multiply desired module speeds by cosine of heading error (#5758)
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2023-10-12 23:20:57 -07:00
Gold856
1c724884ca [build] Add CMake option to build Java source jars (#5756) 2023-10-11 12:50:54 -07:00
621 changed files with 184895 additions and 29034 deletions

View File

@@ -19,7 +19,8 @@ jobs:
- os: macOS-12
name: macOS
container: ""
flags: "-DCMAKE_BUILD_TYPE=Release -DWITH_JAVA=OFF -DWITH_EXAMPLES=ON"
env: "PATH=\"/usr/local/opt/protobuf@3/bin:$PATH\""
flags: "-DCMAKE_BUILD_TYPE=Release -DWITH_JAVA=OFF -DWITH_EXAMPLES=ON -DCMAKE_LIBRARY_PATH=/usr/local/opt/protobuf@3/lib -DProtobuf_INCLUDE_DIR=/usr/local/opt/protobuf@3/include -DProtobuf_PROTOC_EXECUTABLE=/usr/local/opt/protobuf@3/bin/protoc"
name: "Build - ${{ matrix.name }}"
runs-on: ${{ matrix.os }}
@@ -27,10 +28,14 @@ jobs:
steps:
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv4.5-java python-is-python3 ninja-build
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv4.5-java python-is-python3 libprotobuf-dev protobuf-compiler ninja-build
- name: Install QuickBuffers (Linux)
if: runner.os == 'Linux'
run: wget https://github.com/HebiRobotics/QuickBuffers/releases/download/1.3.2/protoc-gen-quickbuf_1.3.2_amd64.deb && sudo apt install ./protoc-gen-quickbuf_1.3.2_amd64.deb
- name: Install opencv (macOS)
run: brew install opencv ninja
run: brew install opencv protobuf@3 ninja
if: runner.os == 'macOS'
- name: Set up Python 3.8 (macOS)
@@ -71,12 +76,6 @@ jobs:
- name: Install CMake
uses: lukka/get-cmake@v3.27.6
- name: Run vcpkg
uses: lukka/run-vcpkg@v11.1
with:
vcpkgDirectory: ${{ runner.workspace }}/vcpkg
vcpkgGitCommitId: 78b61582c9e093fda56a01ebb654be15a0033897 # HEAD on 2023-08-6
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.3
@@ -85,6 +84,12 @@ jobs:
- uses: actions/checkout@v3
- name: Run vcpkg
uses: lukka/run-vcpkg@v11.1
with:
vcpkgDirectory: ${{ runner.workspace }}/vcpkg
vcpkgGitCommitId: 78b61582c9e093fda56a01ebb654be15a0033897 # HEAD on 2023-08-6
- name: configure
run: cmake -S . -B build -G "Ninja" -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DCMAKE_BUILD_TYPE=Release -DWITH_JAVA=OFF -DWITH_EXAMPLES=ON -DUSE_SYSTEM_FMTLIB=ON -DUSE_SYSTEM_LIBUV=ON -DUSE_SYSTEM_EIGEN=ON -DCMAKE_TOOLCHAIN_FILE=${{ runner.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_INSTALL_OPTIONS=--clean-after-build -DVCPKG_TARGET_TRIPLET=x64-windows-release -DVCPKG_HOST_TRIPLET=x64-windows-release
env:

View File

@@ -9,11 +9,11 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: React Rocket
uses: actions/github-script@v4
uses: actions/github-script@v6
with:
script: |
const {owner, repo} = context.issue
github.reactions.createForIssueComment({
github.rest.reactions.createForIssueComment({
owner,
repo,
comment_id: context.payload.comment.id,
@@ -34,7 +34,7 @@ jobs:
GITHUB_TOKEN: "${{ secrets.COMMENT_COMMAND_PAT_TOKEN }}"
NUMBER: ${{ github.event.issue.number }}
- name: Set up Python 3.8
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Setup Java

View File

@@ -193,6 +193,19 @@ jobs:
needs: [build-docker, build-host, build-documentation]
runs-on: ubuntu-22.04
steps:
- name: Free Disk Space
if: |
github.repository_owner == 'wpilibsuite' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: false
docker-images: false
swap-storage: false
- uses: actions/checkout@v3
if: |
github.repository_owner == 'wpilibsuite' &&
@@ -223,7 +236,7 @@ jobs:
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- name: Combine (Main)
if: |
github.repository_owner == 'wpilibsuite' &&

View File

@@ -29,7 +29,11 @@ jobs:
container: wpilib/roborio-cross-ubuntu:2024-22.04
steps:
- name: Install Dependencies
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv4.5-java python-is-python3 clang-14
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv4.5-java python-is-python3 clang-14 libprotobuf-dev protobuf-compiler ninja-build
- name: Install QuickBuffers
if: runner.os == 'Linux'
run: wget https://github.com/HebiRobotics/QuickBuffers/releases/download/1.3.2/protoc-gen-quickbuf_1.3.2_amd64.deb && sudo apt install ./protoc-gen-quickbuf_1.3.2_amd64.deb
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.3
@@ -40,7 +44,7 @@ jobs:
- uses: actions/checkout@v3
- name: configure
run: mkdir build && cd build && cmake -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/clang-14 -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/clang++-14 ${{ matrix.cmake-flags }} ..
run: mkdir build && cd build && cmake -G Ninja -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/clang-14 -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/clang++-14 -DWITH_JAVA=OFF ${{ matrix.cmake-flags }} ..
env:
SCCACHE_GHA_ENABLED: "true"

View File

@@ -41,6 +41,10 @@ jobs:
run: |
cd upstream_utils
./update_gcem.py
- name: Run update_json.py
run: |
cd upstream_utils
./update_json.py
- name: Run update_libuv.py
run: |
cd upstream_utils
@@ -61,6 +65,10 @@ jobs:
run: |
cd upstream_utils
./update_memory.py
- name: Run update_protobuf.py
run: |
cd upstream_utils
./update_protobuf.py
- name: Add untracked files to index so they count as changes
run: git add -A
- name: Check output

View File

@@ -31,6 +31,7 @@ includeOtherLibs {
^cscore
^fmt/
^gtest/
^google/
^hal/
^imgui
^implot

View File

@@ -55,7 +55,8 @@ ENDIF("${isSystemDir}" STREQUAL "-1")
# Options for building certain parts of the repo. Everything is built by default.
option(BUILD_SHARED_LIBS "Build with shared libs (needed for JNI)" ON)
option(WITH_JAVA "Include java and JNI in the build" ON)
option(WITH_JAVA "Include Java and JNI in the build" ON)
option(WITH_JAVA_SOURCE "Build Java source jars" ON)
option(WITH_CSCORE "Build cscore (needs OpenCV)" ON)
option(WITH_NTCORE "Build ntcore" ON)
option(WITH_WPIMATH "Build wpimath" ON)
@@ -176,6 +177,12 @@ endif()
find_package(LIBSSH 0.7.1)
find_package(Protobuf REQUIRED)
find_program(Quickbuf_EXECUTABLE
NAMES protoc-gen-quickbuf
DOC "The Quickbuf protoc plugin"
)
if (WITH_FLAT_INSTALL)
set(WPIUTIL_DEP_REPLACE "include($\{SELF_DIR\}/wpiutil-config.cmake)")
set(WPINET_DEP_REPLACE "include($\{SELF_DIR\}/wpinet-config.cmake)")

View File

@@ -18,9 +18,13 @@ By default, all libraries except for the HAL and WPILib get built with a default
## Prerequisites
The most common prerequisite is going to be OpenCV. OpenCV needs to be findable by CMake. On systems like the Jetson, this is installed by default. Otherwise, you will need to build OpenCV from source and install it.
The jinja2 pip package is needed to generate classes for NT4's pubsub.
In addition, if you want JNI and Java, you will need a JDK of at least version 11 installed. In addition, you need a `JAVA_HOME` environment variable set properly and set to the JDK directory.
The protobuf library and compiler are needed for protobuf generation. The QuickBuffers protoc-gen package is also required when Java is being built; this can be obtained from https://github.com/HebiRobotics/QuickBuffers/releases/.
OpenCV needs to be findable by CMake. On systems like the Jetson, this is installed by default. Otherwise, you will need to build OpenCV from source and install it.
If you want JNI and Java, you will need a JDK of at least version 11 installed. In addition, you need a `JAVA_HOME` environment variable set properly and set to the JDK directory.
If you are building with unit tests or simulation modules, you will also need an Internet connection for the initial setup process, as CMake will clone google-test and imgui from GitHub.
@@ -30,6 +34,8 @@ The following build options are available:
* `WITH_JAVA` (ON Default)
* This option will enable Java and JNI builds. If this is on, `WITH_SHARED_LIBS` must be on. Otherwise CMake will error.
* `WITH_JAVA_SOURCE` (ON Default)
* This option will build Java source JARs for each enabled Java library. This does not require `WITH_JAVA` to be on, allowing source JARs to be built without the compiled JARs if desired.
* `WITH_SHARED_LIBS` (ON Default)
* This option will cause cmake to build static libraries instead of shared libraries. If this is off, `WITH_JAVA` must be off. Otherwise CMake will error.
* `WITH_CSCORE` (ON Default)

View File

@@ -88,9 +88,24 @@ If opening from a fresh clone, generated java dependencies will not exist. Most
`./gradlew build` builds _everything_, which includes debug and release builds for desktop and all installed cross compilers. Many developers don't need or want to build all of this. Therefore, common tasks have shortcuts to only build necessary components for common development and testing tasks.
`./gradlew testDesktopCpp` and `./gradlew testDesktopJava` will build and run the tests for `wpilibc` and `wpilibj` respectively. They will only build the minimum components required to run the tests.
`./gradlew testDesktopCpp` and `./gradlew testDesktopJava` will build and run the tests for `wpilibc` and `wpilibj` respectively. They will only build the minimum components required to run the tests. `./gradlew testDesktop` will run both `testDesktopJava` and `testDesktopCpp`.
`testDesktopCpp` and `testDesktopJava` tasks also exist for the projects `wpiutil`, `ntcore`, `cscore`, `hal` `wpilibNewCommands` and `cameraserver`. These can be ran with `./gradlew :projectName:task`.
`testDesktopCpp`, `testDesktopJava`, and `testDesktop` tasks also exist for the following projects:
- `apriltag`
- `cameraserver`
- `cscore`
- `hal`
- `ntcore`
- `wpilibNewCommands`
- `wpimath`
- `wpinet`
- `wpiunits`
- `wpiutil`
- `romiVendordep`
- `xrpVendordep`
These can be ran with `./gradlew :projectName:task`.
`./gradlew buildDesktopCpp` and `./gradlew buildDesktopJava` will compile `wpilibcExamples` and `wpilibjExamples` respectively. The results can't be ran, but they can compile.

View File

@@ -66,6 +66,21 @@ if (WITH_JAVA)
endif()
if (WITH_JAVA_SOURCE)
find_package(Java REQUIRED)
include(UseJava)
file(GLOB APRILTAG_SOURCES src/main/java/edu/wpi/first/apriltag/*.java)
add_jar(apriltag_src_jar
RESOURCES NAMESPACE "edu/wpi/first/apriltag" ${APRILTAG_SOURCES}
NAMESPACE "edu/wpi/first/apriltag/jni" src/main/java/edu/wpi/first/apriltag/jni/AprilTagJNI.java
OUTPUT_NAME apriltag-sources)
get_property(APRILTAG_SRC_JAR_FILE TARGET apriltag_src_jar PROPERTY JAR_FILE)
install(FILES ${APRILTAG_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
set_property(TARGET apriltag_src_jar PROPERTY FOLDER "java")
endif()
generate_resources(src/main/native/resources/edu/wpi/first/apriltag generated/main/cpp APRILTAG frc apriltag_resources_src)
file(GLOB apriltag_native_src src/main/native/cpp/*.cpp)

View File

@@ -113,6 +113,26 @@ public class AprilTagFieldLayout {
return new ArrayList<>(m_apriltags.values());
}
/**
* Returns the length of the field the layout is representing in meters.
*
* @return length, in meters
*/
@JsonIgnore
public double getFieldLength() {
return m_fieldDimensions.fieldLength;
}
/**
* Returns the length of the field the layout is representing in meters.
*
* @return width, in meters
*/
@JsonIgnore
public double getFieldWidth() {
return m_fieldDimensions.fieldWidth;
}
/**
* Sets the origin based on a predefined enumeration of coordinate frame origins. The origins are
* calculated from the field dimensions.
@@ -152,6 +172,16 @@ public class AprilTagFieldLayout {
m_origin = origin;
}
/**
* Returns the origin used for tag pose transformation.
*
* @return the origin
*/
@JsonIgnore
public Pose3d getOrigin() {
return m_origin;
}
/**
* Gets an AprilTag pose by its ID.
*

View File

@@ -22,7 +22,7 @@ AprilTagFieldLayout::AprilTagFieldLayout(std::string_view path) {
throw std::runtime_error(fmt::format("Cannot open file: {}", path));
}
wpi::json json = wpi::json::parse({fileBuffer->begin(), fileBuffer->end()});
wpi::json json = wpi::json::parse(fileBuffer->begin(), fileBuffer->end());
for (const auto& tag : json.at("tags").get<std::vector<AprilTag>>()) {
m_apriltags[tag.ID] = tag;
@@ -41,6 +41,14 @@ AprilTagFieldLayout::AprilTagFieldLayout(std::vector<AprilTag> apriltags,
}
}
units::meter_t AprilTagFieldLayout::GetFieldLength() const {
return m_fieldLength;
}
units::meter_t AprilTagFieldLayout::GetFieldWidth() const {
return m_fieldWidth;
}
void AprilTagFieldLayout::SetOrigin(OriginPosition origin) {
switch (origin) {
case OriginPosition::kBlueAllianceWallRightSide:
@@ -59,6 +67,10 @@ void AprilTagFieldLayout::SetOrigin(const Pose3d& origin) {
m_origin = origin;
}
Pose3d AprilTagFieldLayout::GetOrigin() const {
return m_origin;
}
std::optional<frc::Pose3d> AprilTagFieldLayout::GetTagPose(int ID) const {
const auto& it = m_apriltags.find(ID);
if (it == m_apriltags.end()) {

View File

@@ -62,6 +62,18 @@ class WPILIB_DLLEXPORT AprilTagFieldLayout {
AprilTagFieldLayout(std::vector<AprilTag> apriltags,
units::meter_t fieldLength, units::meter_t fieldWidth);
/**
* Returns the length of the field the layout is representing.
* @return length
*/
units::meter_t GetFieldLength() const;
/**
* Returns the length of the field the layout is representing.
* @return width
*/
units::meter_t GetFieldWidth() const;
/**
* Sets the origin based on a predefined enumeration of coordinate frame
* origins. The origins are calculated from the field dimensions.
@@ -83,6 +95,12 @@ class WPILIB_DLLEXPORT AprilTagFieldLayout {
*/
void SetOrigin(const Pose3d& origin);
/**
* Returns the origin used for tag pose transformation.
* @return the origin
*/
Pose3d GetOrigin() const;
/**
* Gets an AprilTag pose by its ID.
*

View File

@@ -24,6 +24,7 @@ plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
id 'com.diffplug.spotless' version '6.20.0' apply false
id 'com.github.spotbugs' version '5.1.3' apply false
id 'com.google.protobuf' version '0.9.3' apply false
}
wpilibVersioning.buildServerMode = project.hasProperty('buildServer')

View File

@@ -28,7 +28,7 @@ public class WPIJREArtifact extends MavenArtifact {
private boolean checkJreVersion = true;
private final String artifactLocation = "edu.wpi.first.jdk:roborio-2023:17.0.5u7-1"
private final String artifactLocation = "edu.wpi.first.jdk:roborio-2024:17.0.9u7-1"
@Inject
public WPIJREArtifact(String name, RemoteTarget target) {

View File

@@ -28,6 +28,22 @@ if (WITH_JAVA)
endif()
if (WITH_JAVA_SOURCE)
find_package(Java REQUIRED)
include(UseJava)
file(GLOB CAMERASERVER_SOURCES src/main/java/edu/wpi/first/cameraserver/*.java)
file(GLOB VISION_SOURCES src/main/java/edu/wpi/first/vision/*.java)
add_jar(cameraserver_src_jar
RESOURCES NAMESPACE "edu/wpi/first/cameraserver" ${CAMERASERVER_SOURCES}
NAMESPACE "edu/wpi/first/vision" ${VISION_SOURCES}
OUTPUT_NAME cameraserver-sources)
get_property(CAMERASERVER_SRC_JAR_FILE TARGET cameraserver_src_jar PROPERTY JAR_FILE)
install(FILES ${CAMERASERVER_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
set_property(TARGET cameraserver_src_jar PROPERTY FOLDER "java")
endif()
file(GLOB_RECURSE
cameraserver_native_src src/main/native/cpp/*.cpp)
add_library(cameraserver ${cameraserver_native_src})

View File

@@ -105,7 +105,7 @@ bool ReadConfig() {
// parse file
wpi::json j;
try {
j = wpi::json::parse({fileBuffer->begin(), fileBuffer->end()});
j = wpi::json::parse(fileBuffer->begin(), fileBuffer->end());
} catch (const wpi::json::parse_error& e) {
fmt::print(stderr, "config error in '{}': byte {}: {}\n", configFile,
e.byte, e.what());

View File

@@ -133,6 +133,22 @@ if (WITH_JAVA)
endif()
if (WITH_JAVA_SOURCE)
find_package(Java REQUIRED)
include(UseJava)
file(GLOB CSCORE_SOURCES src/main/java/edu/wpi/first/cscore/*.java)
file(GLOB CSCORE_RAW_SOURCES src/main/java/edu/wpi/first/cscore/raw/*.java)
add_jar(cscore_src_jar
RESOURCES NAMESPACE "edu/wpi/first/cscore" ${CSCORE_SOURCES}
NAMESPACE "edu/wpi/first/cscore/raw" ${CSCORE_RAW_SOURCES}
OUTPUT_NAME cscore-sources)
get_property(CSCORE_SRC_JAR_FILE TARGET cscore_src_jar PROPERTY JAR_FILE)
install(FILES ${CSCORE_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
set_property(TARGET cscore_src_jar PROPERTY FOLDER "java")
endif()
if (WITH_TESTS)
wpilib_add_test(cscore src/test/native/cpp)
target_link_libraries(cscore_test cscore gmock)

View File

@@ -595,7 +595,7 @@ void UsbCameraImpl::DeviceConnect() {
}
if (m_connectVerbose) {
SINFO("Connecting to USB camera on {}", m_path);
SINFO("Attempting to connect to USB camera on {}", m_path);
}
// Try to open the device
@@ -606,6 +606,10 @@ void UsbCameraImpl::DeviceConnect() {
}
m_fd = fd;
if (m_connectVerbose) {
SINFO("Connected to USB camera on {}", m_path);
}
// Get capabilities
SDEBUG3("getting capabilities");
struct v4l2_capability vcap;

View File

@@ -554,7 +554,7 @@ static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) {
}
std::string pathStr = [self.path UTF8String];
OBJCINFO("Connecting to USB camera on {}", pathStr);
OBJCINFO("Attempting to connect to USB camera on {}", pathStr);
self.videoDevice = [AVCaptureDevice deviceWithUniqueID:self.path];
if (self.videoDevice == nil) {
@@ -594,6 +594,8 @@ static cs::VideoMode::PixelFormat FourCCToPixelFormat(FourCharCode fourcc) {
goto err;
}
OBJCINFO("Connected to USB camera on {}", pathStr);
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(sessionRuntimeError:)

View File

@@ -499,7 +499,7 @@ bool UsbCameraImpl::DeviceConnect() {
}
if (m_connectVerbose) {
SINFO("Connecting to USB camera on {}", m_path);
SINFO("Attempting to connect to USB camera on {}", m_path);
}
SDEBUG3("opening device");
@@ -526,6 +526,10 @@ bool UsbCameraImpl::DeviceConnect() {
return false;
}
if (m_connectVerbose) {
SINFO("Connected to USB camera on {}", m_path);
}
CS_Status st = 0;
auto devices = EnumerateUsbCameras(&st);

View File

@@ -32,7 +32,7 @@ class UsbCameraTest {
assertTimeoutPreemptively(
Duration.ofSeconds(5),
() -> assertTrue(result.get().contains("Connecting to USB camera on ")));
() -> assertTrue(result.get().contains("Attempting to connect to USB camera on ")));
}
}

View File

@@ -121,7 +121,13 @@ doxygen {
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/**'
// memory
exclude 'wpi/memory/**'
@@ -136,6 +142,8 @@ doxygen {
//TODO: building memory docs causes search to break
exclude 'wpi/memory/**'
exclude '*.pb.h'
aliases 'effects=\\par <i>Effects:</i>^^',
'notes=\\par <i>Notes:</i>^^',
'requires=\\par <i>Requires:</i>^^',
@@ -227,6 +235,7 @@ task generateJavaDocs(type: Javadoc) {
source project(':wpilibj').sourceSets.main.java
source project(':wpimath').sourceSets.main.java
source project(':wpinet').sourceSets.main.java
source project(':wpiunits').sourceSets.main.java
source project(':wpiutil').sourceSets.main.java
source configurations.javaSource.collect { zipTree(it) }
include '**/*.java'

View File

@@ -24,6 +24,7 @@ includeOtherLibs {
^fmt/
^fields/
^frc/
^google/
^imgui
^networktables/
^ntcore

View File

@@ -9,7 +9,7 @@ include(LinkMacOSGUI)
#
file(GLOB_RECURSE libglass_src src/lib/native/cpp/*.cpp)
add_library(libglass STATIC ${libglass_src})
add_library(libglass ${libglass_src})
set_target_properties(libglass PROPERTIES DEBUG_POSTFIX "d" OUTPUT_NAME "glass")
set_property(TARGET libglass PROPERTY POSITION_INDEPENDENT_CODE ON)
@@ -30,7 +30,7 @@ install(DIRECTORY src/lib/native/include/ DESTINATION "${include_dest}/glass")
#
file(GLOB_RECURSE libglassnt_src src/libnt/native/cpp/*.cpp)
add_library(libglassnt STATIC ${libglassnt_src})
add_library(libglassnt ${libglassnt_src})
set_target_properties(libglassnt PROPERTIES DEBUG_POSTFIX "d" OUTPUT_NAME "glassnt")
set_property(TARGET libglassnt PROPERTY POSITION_INDEPENDENT_CODE ON)

View File

@@ -16,7 +16,6 @@
#include <wpi/StringExtras.h>
#include <wpi/fs.h>
#include <wpi/json.h>
#include <wpi/json_serializer.h>
#include <wpi/raw_ostream.h>
#include <wpi/timestamp.h>
#include <wpigui.h>
@@ -139,7 +138,7 @@ static bool LoadWindowStorageImpl(const std::string& filename) {
} else {
try {
return JsonToWindow(
wpi::json::parse({fileBuffer->begin(), fileBuffer->end()}),
wpi::json::parse(fileBuffer->begin(), fileBuffer->end()),
filename.c_str());
} catch (wpi::json::parse_error& e) {
ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
@@ -166,7 +165,7 @@ static bool LoadStorageRootImpl(Context* ctx, const std::string& filename,
}
try {
storage->FromJson(
wpi::json::parse({fileBuffer->begin(), fileBuffer->end()}),
wpi::json::parse(fileBuffer->begin(), fileBuffer->end()),
filename.c_str());
} catch (wpi::json::parse_error& e) {
ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());

View File

@@ -11,8 +11,8 @@
using namespace glass;
static const char* stations[] = {"Red 1", "Red 2", "Red 3",
"Blue 1", "Blue 2", "Blue 3"};
static const char* stations[] = {"Invalid", "Red 1", "Red 2", "Red 3",
"Blue 1", "Blue 2", "Blue 3"};
void glass::DisplayFMS(FMSModel* model) {
if (!model->Exists() || model->IsReadOnly()) {
@@ -41,7 +41,7 @@ void glass::DisplayFMS(FMSModel* model) {
if (auto data = model->GetAllianceStationIdData()) {
int val = data->GetValue();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
if (ImGui::Combo("Alliance Station", &val, stations, 6)) {
if (ImGui::Combo("Alliance Station", &val, stations, 7)) {
model->SetAllianceStationId(val);
}
data->EmitDrag();

View File

@@ -448,7 +448,7 @@ bool FieldInfo::LoadJson(std::span<const char> is, std::string_view filename) {
// parse file
wpi::json j;
try {
j = wpi::json::parse({is.data(), is.size()});
j = wpi::json::parse(is);
} catch (const wpi::json::parse_error& e) {
fmt::print(stderr, "GUI: JSON: could not parse: {}\n", e.what());
return false;

View File

@@ -14,6 +14,8 @@
#include <vector>
#include <fmt/format.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include <imgui.h>
#include <imgui_stdlib.h>
#include <networktables/NetworkTableInstance.h>
@@ -114,36 +116,37 @@ void NetworkTablesModel::Entry::UpdateInfo(nt::TopicInfo&& info_) {
}
}
static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out,
static void UpdateMsgpackValueSource(NetworkTablesModel& model,
NetworkTablesModel::ValueSource* out,
mpack_reader_t& r, std::string_view name,
int64_t time) {
mpack_tag_t tag = mpack_read_tag(&r);
switch (mpack_tag_type(&tag)) {
case mpack::mpack_type_bool:
out->UpdateFromValue(
nt::Value::MakeBoolean(mpack_tag_bool_value(&tag), time), name, "");
out->value = nt::Value::MakeBoolean(mpack_tag_bool_value(&tag), time);
out->UpdateFromValue(model, name, "");
break;
case mpack::mpack_type_int:
out->UpdateFromValue(
nt::Value::MakeInteger(mpack_tag_int_value(&tag), time), name, "");
out->value = nt::Value::MakeInteger(mpack_tag_int_value(&tag), time);
out->UpdateFromValue(model, name, "");
break;
case mpack::mpack_type_uint:
out->UpdateFromValue(
nt::Value::MakeInteger(mpack_tag_uint_value(&tag), time), name, "");
out->value = nt::Value::MakeInteger(mpack_tag_uint_value(&tag), time);
out->UpdateFromValue(model, name, "");
break;
case mpack::mpack_type_float:
out->UpdateFromValue(
nt::Value::MakeFloat(mpack_tag_float_value(&tag), time), name, "");
out->value = nt::Value::MakeFloat(mpack_tag_float_value(&tag), time);
out->UpdateFromValue(model, name, "");
break;
case mpack::mpack_type_double:
out->UpdateFromValue(
nt::Value::MakeDouble(mpack_tag_double_value(&tag), time), name, "");
out->value = nt::Value::MakeDouble(mpack_tag_double_value(&tag), time);
out->UpdateFromValue(model, name, "");
break;
case mpack::mpack_type_str: {
std::string str;
mpack_read_str(&r, &tag, &str);
out->UpdateFromValue(nt::Value::MakeString(std::move(str), time), name,
"");
out->value = nt::Value::MakeString(std::move(str), time);
out->UpdateFromValue(model, name, "");
break;
}
case mpack::mpack_type_bin:
@@ -164,7 +167,8 @@ static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out,
child.path = fmt::format("{}{}", name, child.name);
}
++i;
UpdateMsgpackValueSource(&child, r, child.path, time); // recurse
UpdateMsgpackValueSource(model, &child, r, child.path,
time); // recurse
}
mpack_done_array(&r);
break;
@@ -186,7 +190,7 @@ static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out,
auto it = elems.find(key);
if (it != elems.end()) {
auto& child = out->valueChildren[it->second];
UpdateMsgpackValueSource(&child, r, child.path, time);
UpdateMsgpackValueSource(model, &child, r, child.path, time);
elems.erase(it);
} else {
added = true;
@@ -194,7 +198,7 @@ static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out,
auto& child = out->valueChildren.back();
child.name = std::move(key);
child.path = fmt::format("{}/{}", name, child.name);
UpdateMsgpackValueSource(&child, r, child.path, time);
UpdateMsgpackValueSource(model, &child, r, child.path, time);
}
}
}
@@ -219,7 +223,318 @@ static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out,
}
}
static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out,
static void UpdateStructValueSource(NetworkTablesModel& model,
NetworkTablesModel::ValueSource* out,
const wpi::DynamicStruct& s,
std::string_view name, int64_t time) {
auto desc = s.GetDescriptor();
out->typeStr = "struct:" + desc->GetName();
auto& fields = desc->GetFields();
if (!out->valueChildrenMap || fields.size() != out->valueChildren.size()) {
out->valueChildren.clear();
out->valueChildrenMap = true;
out->valueChildren.reserve(fields.size());
for (auto&& field : fields) {
out->valueChildren.emplace_back();
auto& child = out->valueChildren.back();
child.name = field.GetName();
child.path = fmt::format("{}/{}", name, child.name);
}
}
auto outIt = out->valueChildren.begin();
for (auto&& field : fields) {
auto& child = *outIt++;
switch (field.GetType()) {
case wpi::StructFieldType::kBool:
if (field.IsArray()) {
std::vector<int> v;
v.reserve(field.GetArraySize());
for (size_t i = 0; i < field.GetArraySize(); ++i) {
v.emplace_back(s.GetBoolField(&field, i));
}
child.value = nt::Value::MakeBooleanArray(std::move(v), time);
} else {
child.value = nt::Value::MakeBoolean(s.GetBoolField(&field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case wpi::StructFieldType::kChar:
child.value = nt::Value::MakeString(s.GetStringField(&field), time);
child.UpdateFromValue(model, child.path, "");
break;
case wpi::StructFieldType::kInt8:
case wpi::StructFieldType::kInt16:
case wpi::StructFieldType::kInt32:
case wpi::StructFieldType::kInt64:
case wpi::StructFieldType::kUint8:
case wpi::StructFieldType::kUint16:
case wpi::StructFieldType::kUint32:
case wpi::StructFieldType::kUint64: {
bool isUint = field.IsUint();
if (field.IsArray()) {
std::vector<int64_t> v;
v.reserve(field.GetArraySize());
for (size_t i = 0; i < field.GetArraySize(); ++i) {
if (isUint) {
v.emplace_back(s.GetUintField(&field, i));
} else {
v.emplace_back(s.GetIntField(&field, i));
}
}
child.value = nt::Value::MakeIntegerArray(std::move(v), time);
} else {
if (isUint) {
child.value = nt::Value::MakeInteger(s.GetUintField(&field), time);
} else {
child.value = nt::Value::MakeInteger(s.GetIntField(&field), time);
}
}
child.UpdateFromValue(model, child.path, "");
break;
}
case wpi::StructFieldType::kFloat:
if (field.IsArray()) {
std::vector<float> v;
v.reserve(field.GetArraySize());
for (size_t i = 0; i < field.GetArraySize(); ++i) {
v.emplace_back(s.GetFloatField(&field, i));
}
child.value = nt::Value::MakeFloatArray(std::move(v), time);
} else {
child.value = nt::Value::MakeFloat(s.GetFloatField(&field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case wpi::StructFieldType::kDouble:
if (field.IsArray()) {
std::vector<double> v;
v.reserve(field.GetArraySize());
for (size_t i = 0; i < field.GetArraySize(); ++i) {
v.emplace_back(s.GetDoubleField(&field, i));
}
child.value = nt::Value::MakeDoubleArray(std::move(v), time);
} else {
child.value = nt::Value::MakeDouble(s.GetDoubleField(&field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case wpi::StructFieldType::kStruct:
if (field.IsArray()) {
if (child.valueChildrenMap) {
child.valueChildren.clear();
child.valueChildrenMap = false;
}
child.valueChildren.resize(field.GetArraySize());
unsigned int i = 0;
for (auto&& child2 : child.valueChildren) {
if (child2.name.empty()) {
child2.name = fmt::format("[{}]", i);
child2.path = fmt::format("{}{}", name, child.name);
}
UpdateStructValueSource(model, &child2, s.GetStructField(&field, i),
child2.path, time); // recurse
++i;
}
} else {
UpdateStructValueSource(model, &child, s.GetStructField(&field),
child.path, time); // recurse
}
break;
}
}
}
static void UpdateProtobufValueSource(NetworkTablesModel& model,
NetworkTablesModel::ValueSource* out,
const google::protobuf::Message& msg,
std::string_view name, int64_t time) {
auto desc = msg.GetDescriptor();
out->typeStr = "proto:" + desc->full_name();
if (!out->valueChildrenMap ||
desc->field_count() != static_cast<int>(out->valueChildren.size())) {
out->valueChildren.clear();
out->valueChildrenMap = true;
out->valueChildren.reserve(desc->field_count());
for (int i = 0, end = desc->field_count(); i < end; ++i) {
out->valueChildren.emplace_back();
auto& child = out->valueChildren.back();
child.name = desc->field(i)->name();
child.path = fmt::format("{}/{}", name, child.name);
}
}
auto refl = msg.GetReflection();
auto outIt = out->valueChildren.begin();
for (int fieldNum = 0, end = desc->field_count(); fieldNum < end;
++fieldNum) {
auto field = desc->field(fieldNum);
auto& child = *outIt++;
switch (field->cpp_type()) {
case google::protobuf::FieldDescriptor::CPPTYPE_BOOL:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<int> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedBool(msg, field, i));
}
child.value = nt::Value::MakeBooleanArray(std::move(v), time);
} else {
child.value = nt::Value::MakeBoolean(refl->GetBool(msg, field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_STRING:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<std::string> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedString(msg, field, i));
}
child.value = nt::Value::MakeStringArray(std::move(v), time);
} else {
child.value =
nt::Value::MakeString(refl->GetString(msg, field), time);
child.UpdateFromValue(model, child.path, "");
}
break;
case google::protobuf::FieldDescriptor::CPPTYPE_INT32:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<int64_t> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedInt32(msg, field, i));
}
child.value = nt::Value::MakeIntegerArray(std::move(v), time);
} else {
child.value =
nt::Value::MakeInteger(refl->GetInt32(msg, field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_INT64:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<int64_t> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedInt64(msg, field, i));
}
child.value = nt::Value::MakeIntegerArray(std::move(v), time);
} else {
child.value =
nt::Value::MakeInteger(refl->GetInt64(msg, field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_UINT32:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<int64_t> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedUInt32(msg, field, i));
}
child.value = nt::Value::MakeIntegerArray(std::move(v), time);
} else {
child.value =
nt::Value::MakeInteger(refl->GetUInt32(msg, field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_UINT64:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<int64_t> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedUInt64(msg, field, i));
}
child.value = nt::Value::MakeIntegerArray(std::move(v), time);
} else {
child.value =
nt::Value::MakeInteger(refl->GetUInt64(msg, field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<float> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedFloat(msg, field, i));
}
child.value = nt::Value::MakeFloatArray(std::move(v), time);
} else {
child.value = nt::Value::MakeFloat(refl->GetFloat(msg, field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<double> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedDouble(msg, field, i));
}
child.value = nt::Value::MakeDoubleArray(std::move(v), time);
} else {
child.value =
nt::Value::MakeDouble(refl->GetDouble(msg, field), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_ENUM:
if (field->is_repeated()) {
size_t size = refl->FieldSize(msg, field);
std::vector<std::string> v;
v.reserve(size);
for (size_t i = 0; i < size; ++i) {
v.emplace_back(refl->GetRepeatedEnum(msg, field, i)->name());
}
child.value = nt::Value::MakeStringArray(std::move(v), time);
} else {
child.value =
nt::Value::MakeString(refl->GetEnum(msg, field)->name(), time);
}
child.UpdateFromValue(model, child.path, "");
break;
case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE:
if (field->is_repeated()) {
if (child.valueChildrenMap) {
child.valueChildren.clear();
child.valueChildrenMap = false;
}
size_t size = refl->FieldSize(msg, field);
child.valueChildren.resize(size);
unsigned int i = 0;
for (auto&& child2 : child.valueChildren) {
if (child2.name.empty()) {
child2.name = fmt::format("[{}]", i);
child2.path = fmt::format("{}{}", name, child.name);
}
UpdateProtobufValueSource(model, &child2,
refl->GetRepeatedMessage(msg, field, i),
child2.path, time); // recurse
++i;
}
} else {
UpdateProtobufValueSource(
model, &child,
refl->GetMessage(msg, field,
model.GetProtobufDatabase().GetMessageFactory()),
child.path, time); // recurse
}
break;
}
}
}
static void UpdateJsonValueSource(NetworkTablesModel& model,
NetworkTablesModel::ValueSource* out,
const wpi::json& j, std::string_view name,
int64_t time) {
switch (j.type()) {
@@ -237,7 +552,7 @@ static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out,
auto it = elems.find(kv.key());
if (it != elems.end()) {
auto& child = out->valueChildren[it->second];
UpdateJsonValueSource(&child, kv.value(), child.path, time);
UpdateJsonValueSource(model, &child, kv.value(), child.path, time);
elems.erase(it);
} else {
added = true;
@@ -245,7 +560,7 @@ static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out,
auto& child = out->valueChildren.back();
child.name = kv.key();
child.path = fmt::format("{}/{}", name, child.name);
UpdateJsonValueSource(&child, kv.value(), child.path, time);
UpdateJsonValueSource(model, &child, kv.value(), child.path, time);
}
}
// erase unmatched keys
@@ -273,30 +588,30 @@ static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out,
child.name = fmt::format("[{}]", i);
child.path = fmt::format("{}{}", name, child.name);
}
UpdateJsonValueSource(&child, j[i++], child.path, time); // recurse
// recurse
UpdateJsonValueSource(model, &child, j[i++], child.path, time);
}
break;
}
case wpi::json::value_t::string:
out->UpdateFromValue(
nt::Value::MakeString(j.get_ref<const std::string&>(), time), name,
"");
out->value = nt::Value::MakeString(j.get_ref<const std::string&>(), time);
out->UpdateFromValue(model, name, "");
break;
case wpi::json::value_t::boolean:
out->UpdateFromValue(nt::Value::MakeBoolean(j.get<bool>(), time), name,
"");
out->value = nt::Value::MakeBoolean(j.get<bool>(), time);
out->UpdateFromValue(model, name, "");
break;
case wpi::json::value_t::number_integer:
out->UpdateFromValue(nt::Value::MakeInteger(j.get<int64_t>(), time), name,
"");
out->value = nt::Value::MakeInteger(j.get<int64_t>(), time);
out->UpdateFromValue(model, name, "");
break;
case wpi::json::value_t::number_unsigned:
out->UpdateFromValue(nt::Value::MakeInteger(j.get<uint64_t>(), time),
name, "");
out->value = nt::Value::MakeInteger(j.get<uint64_t>(), time);
out->UpdateFromValue(model, name, "");
break;
case wpi::json::value_t::number_float:
out->UpdateFromValue(nt::Value::MakeDouble(j.get<double>(), time), name,
"");
out->value = nt::Value::MakeDouble(j.get<double>(), time);
out->UpdateFromValue(model, name, "");
break;
default:
out->value = {};
@@ -336,8 +651,8 @@ void NetworkTablesModel::ValueSource::UpdateDiscreteArray(
}
void NetworkTablesModel::ValueSource::UpdateFromValue(
nt::Value&& v, std::string_view name, std::string_view typeStr) {
value = v;
NetworkTablesModel& model, std::string_view name,
std::string_view typeStr) {
switch (value.type()) {
case NT_BOOLEAN:
UpdateDiscreteSource(name, value.GetBoolean() ? 1 : 0, value.time(),
@@ -381,15 +696,16 @@ void NetworkTablesModel::ValueSource::UpdateFromValue(
child.name = fmt::format("[{}]", i);
child.path = fmt::format("{}{}", name, child.name);
}
child.UpdateFromValue(nt::Value::MakeString(arr[i++], value.time()),
child.path, "");
child.value = nt::Value::MakeString(arr[i++], value.time());
child.UpdateFromValue(model, child.path, "");
}
break;
}
case NT_STRING:
if (typeStr == "json") {
try {
UpdateJsonValueSource(this, wpi::json::parse(value.GetString()), name,
UpdateJsonValueSource(model, this,
wpi::json::parse(value.GetString()), name,
value.last_change());
} catch (wpi::json::exception&) {
// ignore
@@ -407,9 +723,57 @@ void NetworkTablesModel::ValueSource::UpdateFromValue(
if (typeStr == "msgpack") {
mpack_reader_t r;
mpack_reader_init_data(&r, value.GetRaw());
UpdateMsgpackValueSource(this, r, name, value.last_change());
UpdateMsgpackValueSource(model, this, r, name, value.last_change());
mpack_reader_destroy(&r);
} else if (wpi::starts_with(typeStr, "struct:")) {
auto structName = wpi::drop_front(typeStr, 7);
bool isArray = structName.ends_with("[]");
if (isArray) {
structName = wpi::drop_back(structName, 2);
}
auto desc = model.m_structDb.Find(structName);
if (desc && desc->IsValid()) {
if (isArray) {
// array of struct at top level
if (valueChildrenMap) {
valueChildren.clear();
valueChildrenMap = false;
}
auto raw = value.GetRaw();
valueChildren.resize(raw.size() / desc->GetSize());
unsigned int i = 0;
for (auto&& child : valueChildren) {
if (child.name.empty()) {
child.name = fmt::format("[{}]", i);
child.path = fmt::format("{}{}", name, child.name);
}
wpi::DynamicStruct s{desc, raw};
UpdateStructValueSource(model, &child, s, child.path,
value.last_change());
++i;
raw = wpi::drop_front(raw, desc->GetSize());
}
} else {
wpi::DynamicStruct s{desc, value.GetRaw()};
UpdateStructValueSource(model, this, s, name, value.last_change());
}
} else {
valueChildren.clear();
}
} else if (wpi::starts_with(typeStr, "proto:")) {
auto msg = model.m_protoDb.Find(wpi::drop_front(typeStr, 6));
if (msg) {
msg->Clear();
auto raw = value.GetRaw();
if (msg->ParseFromArray(raw.data(), raw.size())) {
UpdateProtobufValueSource(model, this, *msg, name,
value.last_change());
} else {
valueChildren.clear();
}
} else {
valueChildren.clear();
}
} else {
valueChildren.clear();
}
@@ -473,8 +837,8 @@ void NetworkTablesModel::Update() {
} else if (auto valueData = event.GetValueEventData()) {
auto& entry = m_entries[valueData->topic];
if (entry) {
entry->UpdateFromValue(std::move(valueData->value), entry->info.name,
entry->info.type_str);
entry->value = std::move(valueData->value);
entry->UpdateFromValue(*this);
if (wpi::starts_with(entry->info.name, '$') && entry->value.IsRaw() &&
entry->info.type_str == "msgpack") {
// meta topic handling
@@ -499,6 +863,50 @@ void NetworkTablesModel::Update() {
it->second.UpdateSubscribers(entry->value.GetRaw());
}
}
} else if (entry->value.IsRaw() &&
wpi::starts_with(entry->info.name, "/.schema/struct:") &&
entry->info.type_str == "structschema") {
// struct schema handling
auto typeStr = wpi::drop_front(entry->info.name, 16);
std::string_view schema{
reinterpret_cast<const char*>(entry->value.GetRaw().data()),
entry->value.GetRaw().size()};
std::string err;
auto desc = m_structDb.Add(typeStr, schema, &err);
if (!desc) {
fmt::print("could not decode struct '{}' schema '{}': {}\n",
entry->info.name, schema, err);
} else if (desc->IsValid()) {
// loop over all entries with this type and update
for (auto&& entryPair : m_entries) {
auto ts = entryPair.second->info.type_str;
if (!wpi::starts_with(ts, "struct:")) {
continue;
}
ts = wpi::drop_front(ts, 7);
if (ts == typeStr || (wpi::ends_with(ts, "[]") &&
wpi::drop_back(ts, 2) == typeStr)) {
entryPair.second->UpdateFromValue(*this);
}
}
}
} else if (entry->value.IsRaw() &&
wpi::starts_with(entry->info.name, "/.schema/proto:") &&
entry->info.type_str == "proto:FileDescriptorProto") {
// protobuf descriptor handling
auto filename = wpi::drop_front(entry->info.name, 15);
if (!m_protoDb.Add(filename, entry->value.GetRaw())) {
fmt::print("could not decode protobuf '{}' filename '{}'\n",
entry->info.name, filename);
} else {
// loop over all protobuf entries and update (conservatively)
for (auto&& entryPair : m_entries) {
auto& ts = entryPair.second->info.type_str;
if (wpi::starts_with(ts, "proto:")) {
entryPair.second->UpdateFromValue(*this);
}
}
}
}
}
}
@@ -663,24 +1071,89 @@ void NetworkTablesModel::UpdateClients(std::span<const uint8_t> data) {
m_clients = std::move(newClients);
}
static bool GetHeadingTypeString(std::string_view* ts) {
if (wpi::starts_with(*ts, "proto:")) {
*ts = wpi::drop_front(*ts, 6);
auto lastdot = ts->rfind('.');
if (lastdot != std::string_view::npos) {
*ts = wpi::substr(*ts, lastdot + 1);
}
if (wpi::starts_with(*ts, "Protobuf")) {
*ts = wpi::drop_front(*ts, 8);
}
return true;
} else if (wpi::starts_with(*ts, "struct:")) {
*ts = wpi::drop_front(*ts, 7);
return true;
}
return false;
}
static const char* GetShortTypeString(std::string_view ts) {
if (wpi::starts_with(ts, "proto:")) {
return "protobuf";
} else if (wpi::starts_with(ts, "struct:")) {
return "struct";
} else {
return ts.data();
}
}
static const char* GetTypeString(NT_Type type, const char* overrideTypeStr) {
if (overrideTypeStr) {
return GetShortTypeString(overrideTypeStr);
}
switch (type) {
case NT_BOOLEAN:
return "boolean";
case NT_INTEGER:
return "int";
case NT_FLOAT:
return "float";
case NT_DOUBLE:
return "double";
case NT_STRING:
return "string";
case NT_BOOLEAN_ARRAY:
return "boolean[]";
case NT_INTEGER_ARRAY:
return "int[]";
case NT_FLOAT_ARRAY:
return "float[]";
case NT_DOUBLE_ARRAY:
return "double[]";
case NT_STRING_ARRAY:
return "string[]";
case NT_RAW:
return "raw";
case NT_RPC:
return "rpc";
default:
return "other";
}
}
static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry,
const char* typeStr,
const char* overrideTypeStr,
NetworkTablesFlags flags) {
auto& val = entry.value;
if (!val) {
return;
}
const char* typeStr = GetTypeString(val.type(), overrideTypeStr);
ImGui::SetNextItemWidth(
-1 * (ImGui::CalcTextSize(typeStr).x + ImGui::GetStyle().FramePadding.x));
switch (val.type()) {
case NT_BOOLEAN:
ImGui::LabelText(typeStr ? typeStr : "boolean", "%s",
val.GetBoolean() ? "true" : "false");
ImGui::LabelText(typeStr, "%s", val.GetBoolean() ? "true" : "false");
break;
case NT_INTEGER:
ImGui::LabelText(typeStr ? typeStr : "int", "%" PRId64, val.GetInteger());
ImGui::LabelText(typeStr, "%" PRId64, val.GetInteger());
break;
case NT_FLOAT:
ImGui::LabelText(typeStr ? typeStr : "double", "%.6f", val.GetFloat());
ImGui::LabelText(typeStr, "%.6f", val.GetFloat());
break;
case NT_DOUBLE: {
unsigned char precision = (flags & NetworkTablesFlags_Precision) >>
@@ -689,8 +1162,7 @@ static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry,
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
ImGui::LabelText(typeStr ? typeStr : "double",
fmt::format("%.{}f", precision).c_str(),
ImGui::LabelText(typeStr, fmt::format("%.{}f", precision).c_str(),
val.GetDouble());
#ifdef __GNUC__
#pragma GCC diagnostic pop
@@ -698,30 +1170,30 @@ static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry,
break;
}
case NT_STRING: {
ImGui::LabelText(typeStr ? typeStr : "string", "%s",
entry.valueStr.c_str());
ImGui::LabelText(typeStr, "%s", entry.valueStr.c_str());
break;
}
case NT_BOOLEAN_ARRAY:
ImGui::LabelText(typeStr ? typeStr : "boolean[]", "[]");
break;
case NT_INTEGER_ARRAY:
ImGui::LabelText(typeStr ? typeStr : "int[]", "[]");
break;
case NT_FLOAT_ARRAY:
ImGui::LabelText(typeStr ? typeStr : "float[]", "[]");
break;
case NT_DOUBLE_ARRAY:
ImGui::LabelText(typeStr ? typeStr : "double[]", "[]");
break;
case NT_STRING_ARRAY:
ImGui::LabelText(typeStr ? typeStr : "string[]", "[]");
ImGui::LabelText(typeStr, "[]");
break;
case NT_RAW:
ImGui::LabelText(typeStr ? typeStr : "raw", "[...]");
case NT_RAW: {
ImGui::LabelText(typeStr, val.GetRaw().empty() ? "[]" : "[...]");
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (overrideTypeStr) {
ImGui::TextUnformatted(overrideTypeStr);
}
ImGui::Text("%u bytes", static_cast<unsigned int>(val.GetRaw().size()));
ImGui::EndTooltip();
}
break;
}
default:
ImGui::LabelText(typeStr ? typeStr : "other", "?");
ImGui::LabelText(typeStr, "?");
break;
}
}
@@ -883,14 +1355,18 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
return;
}
const char* typeStr =
entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str();
const char* typeStr = GetTypeString(
val.type(),
entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str());
ImGui::SetNextItemWidth(
-1 * (ImGui::CalcTextSize(typeStr).x + ImGui::GetStyle().FramePadding.x));
ImGui::PushID(entry.info.name.c_str());
switch (val.type()) {
case NT_BOOLEAN: {
static const char* boolOptions[] = {"false", "true"};
int v = val.GetBoolean() ? 1 : 0;
if (ImGui::Combo(typeStr ? typeStr : "boolean", &v, boolOptions, 2)) {
if (ImGui::Combo(typeStr, &v, boolOptions, 2)) {
if (entry.publisher == 0) {
entry.publisher =
nt::Publish(entry.info.topic, NT_BOOLEAN, "boolean");
@@ -901,9 +1377,8 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
}
case NT_INTEGER: {
int64_t v = val.GetInteger();
if (ImGui::InputScalar(typeStr ? typeStr : "int", ImGuiDataType_S64, &v,
nullptr, nullptr, nullptr,
ImGuiInputTextFlags_EnterReturnsTrue)) {
if (ImGui::InputScalar(typeStr, ImGuiDataType_S64, &v, nullptr, nullptr,
nullptr, ImGuiInputTextFlags_EnterReturnsTrue)) {
if (entry.publisher == 0) {
entry.publisher = nt::Publish(entry.info.topic, NT_INTEGER, "int");
}
@@ -913,7 +1388,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
}
case NT_FLOAT: {
float v = val.GetFloat();
if (ImGui::InputFloat(typeStr ? typeStr : "float", &v, 0, 0, "%.6f",
if (ImGui::InputFloat(typeStr, &v, 0, 0, "%.6f",
ImGuiInputTextFlags_EnterReturnsTrue)) {
if (entry.publisher == 0) {
entry.publisher = nt::Publish(entry.info.topic, NT_FLOAT, "float");
@@ -930,7 +1405,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
if (ImGui::InputDouble(typeStr ? typeStr : "double", &v, 0, 0,
if (ImGui::InputDouble(typeStr, &v, 0, 0,
fmt::format("%.{}f", precision).c_str(),
ImGuiInputTextFlags_EnterReturnsTrue)) {
if (entry.publisher == 0) {
@@ -945,7 +1420,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
}
case NT_STRING: {
char* v = GetTextBuffer(entry.valueStr);
if (ImGui::InputText(typeStr ? typeStr : "string", v, kTextBufferSize,
if (ImGui::InputText(typeStr, v, kTextBufferSize,
ImGuiInputTextFlags_EnterReturnsTrue)) {
if (v[0] == '"') {
if (entry.publisher == 0) {
@@ -960,7 +1435,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
break;
}
case NT_BOOLEAN_ARRAY:
ImGui::LabelText("boolean[]", "[]");
ImGui::LabelText(typeStr, "[]");
if (ImGui::BeginPopupContextItem("boolean[]")) {
if (ImGui::Selectable("Edit Array")) {
gArrayEditor =
@@ -973,7 +1448,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
}
break;
case NT_INTEGER_ARRAY:
ImGui::LabelText("int[]", "[]");
ImGui::LabelText(typeStr, "[]");
if (ImGui::BeginPopupContextItem("int[]")) {
if (ImGui::Selectable("Edit Array")) {
gArrayEditor =
@@ -986,7 +1461,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
}
break;
case NT_FLOAT_ARRAY:
ImGui::LabelText("float[]", "[]");
ImGui::LabelText(typeStr, "[]");
if (ImGui::BeginPopupContextItem("float[]")) {
if (ImGui::Selectable("Edit Array")) {
gArrayEditor =
@@ -998,7 +1473,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
}
break;
case NT_DOUBLE_ARRAY:
ImGui::LabelText("double[]", "[]");
ImGui::LabelText(typeStr, "[]");
if (ImGui::BeginPopupContextItem("double[]")) {
if (ImGui::Selectable("Edit Array")) {
gArrayEditor =
@@ -1010,7 +1485,7 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
}
break;
case NT_STRING_ARRAY:
ImGui::LabelText("string[]", "[]");
ImGui::LabelText(typeStr, "[]");
if (ImGui::BeginPopupContextItem("string[]")) {
if (ImGui::Selectable("Edit Array")) {
gArrayEditor =
@@ -1022,15 +1497,23 @@ static void EmitEntryValueEditable(NetworkTablesModel* model,
break;
}
break;
case NT_RAW:
ImGui::LabelText(typeStr ? typeStr : "raw",
val.GetRaw().empty() ? "[]" : "[...]");
case NT_RAW: {
ImGui::LabelText(typeStr, val.GetRaw().empty() ? "[]" : "[...]");
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (!entry.info.type_str.empty()) {
ImGui::TextUnformatted(entry.info.type_str.c_str());
}
ImGui::Text("%u bytes", static_cast<unsigned int>(val.GetRaw().size()));
ImGui::EndTooltip();
}
break;
}
case NT_RPC:
ImGui::LabelText(typeStr ? typeStr : "rpc", "[...]");
ImGui::LabelText(typeStr, "[...]");
break;
default:
ImGui::LabelText(typeStr ? typeStr : "other", "?");
ImGui::LabelText(typeStr, "?");
break;
}
ImGui::PopID();
@@ -1164,15 +1647,25 @@ static void EmitValueTree(
ImGui::TableNextColumn();
if (!child.valueChildren.empty()) {
char label[64];
if (child.valueChildrenMap) {
wpi::format_to_n_c_str(label, sizeof(label), "{{...}}##v_{}",
child.name);
} else {
wpi::format_to_n_c_str(label, sizeof(label), "[...]##v_{}", child.name);
auto pos = ImGui::GetCursorPos();
char label[128];
std::string_view ts = child.typeStr;
bool havePopup = GetHeadingTypeString(&ts);
wpi::format_to_n_c_str(label, sizeof(label), "{}##v_{}", ts.data(),
child.name.c_str());
bool valueChildrenOpen =
TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth);
if (havePopup) {
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(child.typeStr.c_str());
ImGui::EndTooltip();
}
}
if (TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth)) {
// make it look like a normal label w/type
ImGui::SetCursorPos(pos);
ImGui::LabelText(child.valueChildrenMap ? "{...}" : "[...]", "%s", "");
if (valueChildrenOpen) {
EmitValueTree(child.valueChildren, flags);
TreePop();
}
@@ -1197,22 +1690,29 @@ static void EmitEntry(NetworkTablesModel* model,
ImGui::TableNextColumn();
if (!entry.valueChildren.empty()) {
auto pos = ImGui::GetCursorPos();
char label[64];
if (entry.valueChildrenMap) {
wpi::format_to_n_c_str(label, sizeof(label), "{{...}}##v_{}",
entry.info.name.c_str());
} else {
wpi::format_to_n_c_str(label, sizeof(label), "[...]##v_{}",
entry.info.name.c_str());
}
char label[128];
std::string_view ts = entry.info.type_str;
bool havePopup = GetHeadingTypeString(&ts);
wpi::format_to_n_c_str(label, sizeof(label), "{}##v_{}", ts.data(),
entry.info.name.c_str());
valueChildrenOpen =
TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth |
ImGuiTreeNodeFlags_AllowItemOverlap);
if (havePopup) {
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(entry.info.type_str.c_str());
ImGui::EndTooltip();
}
}
// make it look like a normal label w/type
const char* typeStr = GetTypeString(
NT_RAW,
entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str());
ImGui::SetCursorPos(pos);
ImGui::LabelText(entry.info.type_str.c_str(), "%s", "");
ImGui::SetNextItemWidth(-1 * (ImGui::CalcTextSize(typeStr).x +
ImGui::GetStyle().FramePadding.x));
ImGui::LabelText(typeStr, "%s", "");
if ((entry.value.IsBooleanArray() || entry.value.IsFloatArray() ||
entry.value.IsDoubleArray() || entry.value.IsIntegerArray() ||
entry.value.IsStringArray()) &&

View File

@@ -18,6 +18,8 @@
#include <ntcore_cpp.h>
#include <wpi/DenseMap.h>
#include <wpi/json.h>
#include <wpi/protobuf/ProtobufMessageDatabase.h>
#include <wpi/struct/DynamicStruct.h>
#include "glass/Model.h"
#include "glass/View.h"
@@ -31,7 +33,7 @@ class NetworkTablesModel : public Model {
struct EntryValueTreeNode;
struct ValueSource {
void UpdateFromValue(nt::Value&& v, std::string_view name,
void UpdateFromValue(NetworkTablesModel& model, std::string_view name,
std::string_view typeStr);
/** The latest value. */
@@ -40,6 +42,9 @@ class NetworkTablesModel : public Model {
/** String representation of the value (for arrays / complex values). */
std::string valueStr;
/** Data type */
std::string typeStr;
/** Data source (for numeric values). */
std::unique_ptr<DataSource> source;
@@ -73,6 +78,10 @@ class NetworkTablesModel : public Model {
Entry& operator=(const Entry&) = delete;
~Entry();
void UpdateFromValue(NetworkTablesModel& model) {
ValueSource::UpdateFromValue(model, info.name, info.type_str);
}
void UpdateTopic(nt::Event&& event) {
if (std::holds_alternative<nt::TopicInfo>(event.data)) {
UpdateInfo(std::get<nt::TopicInfo>(std::move(event.data)));
@@ -158,6 +167,9 @@ class NetworkTablesModel : public Model {
Entry* GetEntry(std::string_view name);
Entry* AddEntry(NT_Topic topic);
wpi::StructDescriptorDatabase& GetStructDatabase() { return m_structDb; }
wpi::ProtobufMessageDatabase& GetProtobufDatabase() { return m_protoDb; }
private:
void RebuildTree();
void RebuildTreeImpl(std::vector<TreeNode>* tree, int category);
@@ -177,6 +189,9 @@ class NetworkTablesModel : public Model {
std::map<std::string, Client, std::less<>> m_clients;
Client m_server;
wpi::StructDescriptorDatabase m_structDb;
wpi::ProtobufMessageDatabase m_protoDb;
};
using NetworkTablesFlags = int;

View File

@@ -80,12 +80,10 @@ if (WITH_JAVA)
file(GLOB_RECURSE hal_shared_jni_src src/main/native/cpp/jni/*.cpp)
file(GLOB_RECURSE JAVA_SOURCES
${CMAKE_CURRENT_BINARY_DIR}/FRCNetComm.java
src/main/java/*.java)
file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
set(CMAKE_JNI_TARGET true)
add_jar(hal_jar ${JAVA_SOURCES} INCLUDE_JARS wpiutil_jar OUTPUT_NAME wpiHal GENERATE_NATIVE_HEADERS hal_jni_headers)
add_jar(hal_jar ${JAVA_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/FRCNetComm.java INCLUDE_JARS wpiutil_jar OUTPUT_NAME wpiHal GENERATE_NATIVE_HEADERS hal_jni_headers)
get_property(HAL_JAR_FILE TARGET hal_jar PROPERTY JAR_FILE)
install(FILES ${HAL_JAR_FILE} DESTINATION "${java_lib_dest}")
@@ -112,6 +110,27 @@ if (WITH_JAVA)
endif()
if (WITH_JAVA_SOURCE)
find_package(Java REQUIRED)
include(UseJava)
file(GLOB HAL_SOURCES src/main/java/edu/wpi/first/hal/*.java ${CMAKE_CURRENT_BINARY_DIR}/FRCNetComm.java)
file(GLOB HAL_CAN_SOURCES src/main/java/edu/wpi/first/hal/can/*.java)
file(GLOB HAL_SIMULATION_SOURCES src/main/java/edu/wpi/first/hal/simulation/*.java)
file(GLOB HAL_UTIL_SOURCES src/main/java/edu/wpi/first/hal/util/*.java)
add_jar(hal_src_jar
RESOURCES NAMESPACE "edu/wpi/first/hal" ${HAL_SOURCES}
NAMESPACE "edu/wpi/first/hal/can" ${HAL_CAN_SOURCES}
NAMESPACE "edu/wpi/first/hal/communication" src/main/java/edu/wpi/first/hal/communication/NIRioStatus.java
NAMESPACE "edu/wpi/first/hal/simulation" ${HAL_SIMULATION_SOURCES}
NAMESPACE "edu/wpi/first/hal/util" ${HAL_UTIL_SOURCES}
OUTPUT_NAME wpiHal-sources)
get_property(HAL_SRC_JAR_FILE TARGET hal_src_jar PROPERTY JAR_FILE)
install(FILES ${HAL_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
set_property(TARGET hal_src_jar PROPERTY FOLDER "java")
endif()
if (WITH_TESTS)
wpilib_add_test(hal src/test/native/cpp)
target_link_libraries(hal_test hal gtest)

View File

@@ -92,4 +92,11 @@ kResourceType_TrapezoidProfile = 90
kResourceType_DutyCycle = 91
kResourceType_AddressableLEDs = 92
kResourceType_FusionVenom = 93
kResourceType_PS4Controller = 94
kResourceType_CTRE_future7 = 94
kResourceType_CTRE_future8 = 95
kResourceType_CTRE_future9 = 96
kResourceType_CTRE_future10 = 97
kResourceType_CTRE_future11 = 98
kResourceType_CTRE_future12 = 99
kResourceType_CTRE_future13 = 100
kResourceType_CTRE_future14 = 101

View File

@@ -102,8 +102,11 @@ void JoystickDataCache::Update() {
HAL_GetJoystickPOVsInternal(i, &povs[i]);
HAL_GetJoystickButtonsInternal(i, &buttons[i]);
}
FRC_NetworkCommunication_getAllianceStation(
reinterpret_cast<AllianceStationID_t*>(&allianceStation));
AllianceStationID_t alliance = kAllianceStationID_red1;
FRC_NetworkCommunication_getAllianceStation(&alliance);
int allianceInt = alliance;
allianceInt += 1;
allianceStation = static_cast<HAL_AllianceStationID>(allianceInt);
FRC_NetworkCommunication_getMatchTime(&matchTime);
FRC_NetworkCommunication_getControlWord(
reinterpret_cast<ControlWord_t*>(&controlWord));

View File

@@ -29,7 +29,7 @@ DEFINE_CAPI(int32_t, UserFaults5V, 0)
DEFINE_CAPI(int32_t, UserFaults3V3, 0)
DEFINE_CAPI(double, BrownoutVoltage, 6.75)
DEFINE_CAPI(double, CPUTemp, 45.0)
DEFINE_CAPI(int32_t, TeamNumber, 0);
DEFINE_CAPI(int32_t, TeamNumber, 0)
int32_t HALSIM_RegisterRoboRioSerialNumberCallback(
HAL_RoboRioStringCallback callback, void* param, HAL_Bool initialNotify) {

View File

@@ -56,7 +56,9 @@ HAL_ENUM(HAL_CANManufacturer) {
HAL_CAN_Man_kPWF = 11,
HAL_CAN_Man_kStudica = 12,
HAL_CAN_Man_kTheThriftyBot = 13,
HAL_CAN_Man_kReduxRobotics = 14
HAL_CAN_Man_kReduxRobotics = 14,
HAL_CAN_Man_kAndyMark = 15,
HAL_CAN_Man_kVividHosting = 16
};
// clang-format on
/** @} */

View File

@@ -205,7 +205,7 @@ void HAL_SetPWMPeriodScale(HAL_DigitalHandle pwmPortHandle, int32_t squelchMask,
int32_t* status);
/**
* Sets the PWM output to be a continous high signal while enabled.
* Sets the PWM output to be a continuous high signal while enabled.
*
* @param[in] pwmPortHandle the PWM handle.
* @param[out] status Error status variable. 0 on success.

View File

@@ -194,7 +194,7 @@ int32_t HAL_GetControlWord(HAL_ControlWord* controlWord) {
HAL_AllianceStationID HAL_GetAllianceStation(int32_t* status) {
if (gShutdown) {
return HAL_AllianceStationID_kRed1;
return HAL_AllianceStationID_kUnknown;
}
std::scoped_lock lock{driverStation->cacheMutex};
return currentRead->allianceStation;

View File

@@ -69,15 +69,19 @@ endif()
file(GLOB imgui_sources ${imgui_SOURCE_DIR}/*.cpp ${imgui_SOURCE_DIR}/misc/cpp/*.cpp)
file(GLOB implot_sources ${implot_SOURCE_DIR}/*.cpp)
file(GLOB fonts_sources ${fonts_SOURCE_DIR}/src/*.cpp)
add_library(imgui STATIC
set(imgui_all_sources
${imgui_sources}
${implot_sources}
${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
${gl3w_BINARY_DIR}/src/gl3w.c
${fonts_sources}
src/stb_image.cpp
)
src/stb_image.cpp)
if (MSVC)
add_library(imgui STATIC ${imgui_all_sources})
else()
add_library(imgui ${imgui_all_sources})
endif()
target_compile_definitions(imgui PUBLIC IMGUI_IMPL_OPENGL_LOADER_GL3W)
if (MSVC)
target_sources(imgui PRIVATE ${imgui_SOURCE_DIR}/backends/imgui_impl_dx11.cpp)

View File

@@ -54,6 +54,11 @@ if (WITH_JAVA)
include(UseJava)
set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
file(GLOB QUICKBUF_JAR
${WPILIB_BINARY_DIR}/wpiutil/thirdparty/quickbuf/*.jar)
set(CMAKE_JAVA_INCLUDE_PATH wpimath.jar ${QUICKBUF_JAR})
file(GLOB ntcore_jni_src
src/main/native/cpp/jni/*.cpp
${WPILIB_BINARY_DIR}/ntcore/generated/main/native/cpp/jni/*.cpp)
@@ -85,6 +90,20 @@ if (WITH_JAVA)
endif()
if (WITH_JAVA_SOURCE)
find_package(Java REQUIRED)
include(UseJava)
file(GLOB NTCORE_SOURCES src/main/java/edu/wpi/first/networktables/*.java ${WPILIB_BINARY_DIR}/ntcore/generated/*.java)
add_jar(ntcore_src_jar
RESOURCES NAMESPACE "edu/wpi/first/networktables" ${NTCORE_SOURCES}
OUTPUT_NAME ntcore-sources)
get_property(NTCORE_SRC_JAR_FILE TARGET ntcore_src_jar PROPERTY JAR_FILE)
install(FILES ${NTCORE_SRC_JAR_FILE} DESTINATION "${java_lib_dest}")
set_property(TARGET ntcore_src_jar PROPERTY FOLDER "java")
endif()
add_executable(ntcoredev src/dev/native/cpp/main.cpp)
wpilib_target_warnings(ntcoredev)
target_link_libraries(ntcoredev ntcore)

View File

@@ -7,16 +7,22 @@ package edu.wpi.first.networktables;
import edu.wpi.first.util.WPIUtilJNI;
import edu.wpi.first.util.concurrent.Event;
import edu.wpi.first.util.datalog.DataLog;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import us.hebi.quickbuf.ProtoMessage;
/**
* NetworkTables Instance.
@@ -86,6 +92,7 @@ public final class NetworkTableInstance implements AutoCloseable {
public synchronized void close() {
if (m_owned && m_handle != 0) {
m_listeners.close();
m_schemas.forEach((k, v) -> v.close());
NetworkTablesJNI.destroyInstance(m_handle);
m_handle = 0;
}
@@ -176,15 +183,119 @@ public final class NetworkTableInstance implements AutoCloseable {
handle = topic.getHandle();
}
topic = new {{ t.TypeName }}Topic(this, handle);
m_topics.put(name, topic);
{{ t.TypeName }}Topic wrapTopic = new {{ t.TypeName }}Topic(this, handle);
m_topics.put(name, wrapTopic);
// also cache by handle
m_topicsByHandle.put(handle, topic);
m_topicsByHandle.put(handle, wrapTopic);
return ({{ t.TypeName }}Topic) topic;
return wrapTopic;
}
{% endfor %}
/**
* Get protobuf-encoded value topic.
*
* @param <T> value class (inferred from proto)
* @param <MessageType> protobuf message type (inferred from proto)
* @param name topic name
* @param proto protobuf serialization implementation
* @return ProtobufTopic
*/
public <T, MessageType extends ProtoMessage<?>>
ProtobufTopic<T> getProtobufTopic(String name, Protobuf<T, MessageType> proto) {
Topic topic = m_topics.get(name);
if (topic instanceof ProtobufTopic<?>
&& ((ProtobufTopic<?>) topic).getProto().equals(proto)) {
@SuppressWarnings("unchecked")
ProtobufTopic<T> wrapTopic = (ProtobufTopic<T>) topic;
return wrapTopic;
}
int handle;
if (topic == null) {
handle = NetworkTablesJNI.getTopic(m_handle, name);
} else {
handle = topic.getHandle();
}
ProtobufTopic<T> wrapTopic = ProtobufTopic.wrap(this, handle, proto);
m_topics.put(name, wrapTopic);
// also cache by handle
m_topicsByHandle.put(handle, wrapTopic);
return wrapTopic;
}
/**
* Get struct-encoded value topic.
*
* @param <T> value class (inferred from struct)
* @param name topic name
* @param struct struct serialization implementation
* @return StructTopic
*/
public <T>
StructTopic<T> getStructTopic(String name, Struct<T> struct) {
Topic topic = m_topics.get(name);
if (topic instanceof StructTopic<?>
&& ((StructTopic<?>) topic).getStruct().equals(struct)) {
@SuppressWarnings("unchecked")
StructTopic<T> wrapTopic = (StructTopic<T>) topic;
return wrapTopic;
}
int handle;
if (topic == null) {
handle = NetworkTablesJNI.getTopic(m_handle, name);
} else {
handle = topic.getHandle();
}
StructTopic<T> wrapTopic = StructTopic.wrap(this, handle, struct);
m_topics.put(name, wrapTopic);
// also cache by handle
m_topicsByHandle.put(handle, wrapTopic);
return wrapTopic;
}
/**
* Get struct-encoded value array topic.
*
* @param <T> value class (inferred from struct)
* @param name topic name
* @param struct struct serialization implementation
* @return StructArrayTopic
*/
public <T>
StructArrayTopic<T> getStructArrayTopic(String name, Struct<T> struct) {
Topic topic = m_topics.get(name);
if (topic instanceof StructArrayTopic<?>
&& ((StructArrayTopic<?>) topic).getStruct().equals(struct)) {
@SuppressWarnings("unchecked")
StructArrayTopic<T> wrapTopic = (StructArrayTopic<T>) topic;
return wrapTopic;
}
int handle;
if (topic == null) {
handle = NetworkTablesJNI.getTopic(m_handle, name);
} else {
handle = topic.getHandle();
}
StructArrayTopic<T> wrapTopic = StructArrayTopic.wrap(this, handle, struct);
m_topics.put(name, wrapTopic);
// also cache by handle
m_topicsByHandle.put(handle, wrapTopic);
return wrapTopic;
}
private Topic[] topicHandlesToTopics(int[] handles) {
Topic[] topics = new Topic[handles.length];
for (int i = 0; i < handles.length; i++) {
@@ -1050,6 +1161,78 @@ public final class NetworkTableInstance implements AutoCloseable {
return m_listeners.addLogger(minLevel, maxLevel, func);
}
/**
* Returns whether there is a data schema already registered with the given name that this
* instance has published. This does NOT perform a check as to whether the schema has already
* been published by another node on the network.
*
* @param name Name (the string passed as the data type for topics using this schema)
* @return True if schema already registered
*/
public boolean hasSchema(String name) {
return m_schemas.containsKey("/.schema/" + name);
}
/**
* Registers a data schema. Data schemas provide information for how a certain data type string
* can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
* "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas
* are published just like normal topics, with the name being generated from the provided name:
* "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
*
* @param name Name (the string passed as the data type for topics using this schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
public void addSchema(String name, String type, byte[] schema) {
m_schemas.computeIfAbsent("/.schema/" + name, k -> {
RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}");
pub.setDefault(schema);
return pub;
});
}
/**
* Registers a data schema. Data schemas provide information for how a certain data type string
* can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
* "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas
* are published just like normal topics, with the name being generated from the provided name:
* "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
*
* @param name Name (the string passed as the data type for topics using this schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
public void addSchema(String name, String type, String schema) {
m_schemas.computeIfAbsent("/.schema/" + name, k -> {
RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}");
pub.setDefault(StandardCharsets.UTF_8.encode(schema));
return pub;
});
}
/**
* Registers a protobuf schema. Duplicate calls to this function with the same name are silently
* ignored.
*
* @param proto protobuf serialization object
*/
public void addSchema(Protobuf<?, ?> proto) {
proto.forEachDescriptor(
this::hasSchema,
(typeString, schema) -> addSchema(typeString, "proto:FileDescriptorProto", schema));
}
/**
* Registers a struct schema. Duplicate calls to this function with the same name are silently
* ignored.
*
* @param struct struct serialization object
*/
public void addSchema(Struct<?> struct) {
addSchemaImpl(struct, new HashSet<>());
}
@Override
public boolean equals(Object other) {
if (other == this) {
@@ -1067,6 +1250,22 @@ public final class NetworkTableInstance implements AutoCloseable {
return m_handle;
}
private void addSchemaImpl(Struct<?> struct, Set<String> seen) {
String typeString = struct.getTypeString();
if (hasSchema(typeString)) {
return;
}
if (!seen.add(typeString)) {
throw new UnsupportedOperationException(typeString + ": circular reference with " + seen);
}
addSchema(typeString, "structschema", struct.getSchema());
for (Struct<?> inner : struct.getNested()) {
addSchemaImpl(inner, seen);
}
seen.remove(typeString);
}
private boolean m_owned;
private int m_handle;
private final ConcurrentMap<String, RawPublisher> m_schemas = new ConcurrentHashMap<>();
}

View File

@@ -4,6 +4,8 @@
package edu.wpi.first.networktables;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
@@ -13,8 +15,10 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import us.hebi.quickbuf.ProtoMessage;
/** A network table that knows its subtable path. */
@SuppressWarnings("PMD.CouplingBetweenObjects")
public final class NetworkTable {
/** The path separator for sub-tables and keys. */
public static final char PATH_SEPARATOR = '/';
@@ -250,6 +254,44 @@ public final class NetworkTable {
return m_inst.getStringArrayTopic(m_pathWithSep + name);
}
/**
* Get protobuf-encoded value topic.
*
* @param <T> value class (inferred from proto)
* @param <MessageType> protobuf message type (inferred from proto)
* @param name topic name
* @param proto protobuf serialization implementation
* @return ProtobufTopic
*/
public <T, MessageType extends ProtoMessage<?>> ProtobufTopic<T> getProtobufTopic(
String name, Protobuf<T, MessageType> proto) {
return m_inst.getProtobufTopic(m_pathWithSep + name, proto);
}
/**
* Get struct-encoded value topic.
*
* @param <T> value class (inferred from struct)
* @param name topic name
* @param struct struct serialization implementation
* @return StructTopic
*/
public <T> StructTopic<T> getStructTopic(String name, Struct<T> struct) {
return m_inst.getStructTopic(m_pathWithSep + name, struct);
}
/**
* Get struct-encoded value array topic.
*
* @param <T> value class (inferred from struct)
* @param name topic name
* @param struct struct serialization implementation
* @return StructTopic
*/
public <T> StructArrayTopic<T> getStructArrayTopic(String name, Struct<T> struct) {
return m_inst.getStructArrayTopic(m_pathWithSep + name, struct);
}
private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>();
/**

View File

@@ -0,0 +1,17 @@
// 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.networktables;
/**
* NetworkTables protobuf-encoded value entry.
*
* <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
*
* @param <T> value class
*/
public interface ProtobufEntry<T> extends ProtobufSubscriber<T>, ProtobufPublisher<T> {
/** Stops publishing the entry if it's published. */
void unpublish();
}

View File

@@ -0,0 +1,209 @@
// 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.networktables;
import edu.wpi.first.util.protobuf.ProtobufBuffer;
import java.io.IOException;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
/**
* NetworkTables protobuf-encoded value implementation.
*
* @param <T> value class
*/
final class ProtobufEntryImpl<T> extends EntryBase implements ProtobufEntry<T> {
/**
* Constructor.
*
* @param topic Topic
* @param handle Native handle
* @param defaultValue Default value for get()
*/
ProtobufEntryImpl(
ProtobufTopic<T> topic,
ProtobufBuffer<T, ?> buf,
int handle,
T defaultValue,
boolean schemaPublished) {
super(handle);
m_topic = topic;
m_defaultValue = defaultValue;
m_buf = buf;
m_schemaPublished = schemaPublished;
}
@Override
public ProtobufTopic<T> getTopic() {
return m_topic;
}
@Override
public T get() {
return fromRaw(NetworkTablesJNI.getRaw(m_handle, m_emptyRaw), m_defaultValue);
}
@Override
public T get(T defaultValue) {
return fromRaw(NetworkTablesJNI.getRaw(m_handle, m_emptyRaw), defaultValue);
}
@Override
public boolean getInto(T out) {
byte[] raw = NetworkTablesJNI.getRaw(m_handle, m_emptyRaw);
if (raw.length == 0) {
return false;
}
try {
synchronized (m_buf) {
m_buf.readInto(out, raw);
return true;
}
} catch (IOException e) {
// ignored
}
return false;
}
@Override
public TimestampedObject<T> getAtomic() {
return fromRaw(NetworkTablesJNI.getAtomicRaw(m_handle, m_emptyRaw), m_defaultValue);
}
@Override
public TimestampedObject<T> getAtomic(T defaultValue) {
return fromRaw(NetworkTablesJNI.getAtomicRaw(m_handle, m_emptyRaw), defaultValue);
}
@Override
public TimestampedObject<T>[] readQueue() {
TimestampedRaw[] raw = NetworkTablesJNI.readQueueRaw(m_handle);
@SuppressWarnings("unchecked")
TimestampedObject<T>[] arr = (TimestampedObject<T>[]) new TimestampedObject<?>[raw.length];
int err = 0;
for (int i = 0; i < raw.length; i++) {
arr[i] = fromRaw(raw[i], null);
if (arr[i].value == null) {
err++;
}
}
// discard bad values
if (err > 0) {
@SuppressWarnings("unchecked")
TimestampedObject<T>[] newArr =
(TimestampedObject<T>[]) new TimestampedObject<?>[raw.length - err];
int i = 0;
for (TimestampedObject<T> e : arr) {
if (e.value != null) {
arr[i] = e;
i++;
}
}
arr = newArr;
}
return arr;
}
@Override
public T[] readQueueValues() {
byte[][] raw = NetworkTablesJNI.readQueueValuesRaw(m_handle);
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(m_topic.getProto().getTypeClass(), raw.length);
int err = 0;
for (int i = 0; i < raw.length; i++) {
arr[i] = fromRaw(raw[i], null);
if (arr[i] == null) {
err++;
}
}
// discard bad values
if (err > 0) {
@SuppressWarnings("unchecked")
T[] newArr = (T[]) Array.newInstance(m_topic.getProto().getTypeClass(), raw.length - err);
int i = 0;
for (T e : arr) {
if (e != null) {
arr[i] = e;
i++;
}
}
arr = newArr;
}
return arr;
}
@Override
public void set(T value, long time) {
try {
synchronized (m_buf) {
if (!m_schemaPublished) {
m_schemaPublished = true;
m_topic.getInstance().addSchema(m_buf.getProto());
}
ByteBuffer bb = m_buf.write(value);
NetworkTablesJNI.setRaw(m_handle, time, bb, 0, bb.position());
}
} catch (IOException e) {
// ignore
}
}
@Override
public void setDefault(T value) {
try {
synchronized (m_buf) {
if (!m_schemaPublished) {
m_schemaPublished = true;
m_topic.getInstance().addSchema(m_buf.getProto());
}
ByteBuffer bb = m_buf.write(value);
NetworkTablesJNI.setDefaultRaw(m_handle, 0, bb, 0, bb.position());
}
} catch (IOException e) {
// ignore
}
}
@Override
public void unpublish() {
NetworkTablesJNI.unpublish(m_handle);
}
private T fromRaw(byte[] raw, T defaultValue) {
if (raw.length == 0) {
return defaultValue;
}
try {
synchronized (m_buf) {
return m_buf.read(raw);
}
} catch (IOException e) {
return defaultValue;
}
}
private TimestampedObject<T> fromRaw(TimestampedRaw raw, T defaultValue) {
if (raw.value.length == 0) {
return new TimestampedObject<T>(0, 0, defaultValue);
}
try {
synchronized (m_buf) {
return new TimestampedObject<T>(raw.timestamp, raw.serverTime, m_buf.read(raw.value));
}
} catch (IOException e) {
return new TimestampedObject<T>(0, 0, defaultValue);
}
}
private final ProtobufTopic<T> m_topic;
private final T m_defaultValue;
private final ProtobufBuffer<T, ?> m_buf;
private boolean m_schemaPublished;
private static final byte[] m_emptyRaw = new byte[] {};
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.networktables;
import java.util.function.Consumer;
/**
* NetworkTables protobuf-encoded value publisher.
*
* @param <T> value class
*/
public interface ProtobufPublisher<T> extends Publisher, Consumer<T> {
/**
* Get the corresponding topic.
*
* @return Topic
*/
@Override
ProtobufTopic<T> getTopic();
/**
* Publish a new value using current NT time.
*
* @param value value to publish
*/
default void set(T value) {
set(value, 0);
}
/**
* Publish a new value.
*
* @param value value to publish
* @param time timestamp; 0 indicates current NT time should be used
*/
void set(T value, long time);
/**
* Publish a default value. On reconnect, a default value will never be used in preference to a
* published value.
*
* @param value value
*/
void setDefault(T value);
@Override
default void accept(T value) {
set(value);
}
}

View File

@@ -0,0 +1,94 @@
// 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.networktables;
import java.util.function.Supplier;
/**
* NetworkTables protobuf-encoded value subscriber.
*
* @param <T> value class
*/
@SuppressWarnings("PMD.MissingOverride")
public interface ProtobufSubscriber<T> extends Subscriber, Supplier<T> {
/**
* Get the corresponding topic.
*
* @return Topic
*/
@Override
ProtobufTopic<T> getTopic();
/**
* Get the last published value. If no value has been published or the value cannot be unpacked,
* returns the stored default value.
*
* @return value
*/
T get();
/**
* Get the last published value. If no value has been published or the value cannot be unpacked,
* returns the passed defaultValue.
*
* @param defaultValue default value to return if no value has been published
* @return value
*/
T get(T defaultValue);
/**
* Get the last published value, replacing the contents in place of an existing object. If no
* value has been published or the value cannot be unpacked, does not replace the contents and
* returns false. This function will not work (will throw UnsupportedOperationException) unless T
* is mutable (and the implementation of Struct implements unpackInto).
*
* <p>Note: due to Java language limitations, it's not possible to validate at compile time that
* the out parameter is mutable.
*
* @param out object to replace contents of; must be mutable
* @return true if successful
* @throws UnsupportedOperationException if T is immutable
*/
boolean getInto(T out);
/**
* Get the last published value along with its timestamp. If no value has been published or the
* value cannot be unpacked, returns the stored default value and a timestamp of 0.
*
* @return timestamped value
*/
TimestampedObject<T> getAtomic();
/**
* Get the last published value along with its timestamp. If no value has been published or the
* value cannot be unpacked, returns the passed defaultValue and a timestamp of 0.
*
* @param defaultValue default value to return if no value has been published
* @return timestamped value
*/
TimestampedObject<T> getAtomic(T defaultValue);
/**
* Get an array of all valid value changes since the last call to readQueue. Also provides a
* timestamp for each value. Values that cannot be unpacked are dropped.
*
* <p>The "poll storage" subscribe option can be used to set the queue depth.
*
* @return Array of timestamped values; empty array if no valid new changes have been published
* since the previous call.
*/
TimestampedObject<T>[] readQueue();
/**
* Get an array of all valid value changes since the last call to readQueue. Values that cannot be
* unpacked are dropped.
*
* <p>The "poll storage" subscribe option can be used to set the queue depth.
*
* @return Array of values; empty array if no valid new changes have been published since the
* previous call.
*/
T[] readQueueValues();
}

View File

@@ -0,0 +1,178 @@
// 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.networktables;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.protobuf.ProtobufBuffer;
/**
* NetworkTables protobuf-encoded value topic.
*
* @param <T> value class
*/
public final class ProtobufTopic<T> extends Topic {
private ProtobufTopic(Topic topic, Protobuf<T, ?> proto) {
super(topic.m_inst, topic.m_handle);
m_proto = proto;
}
private ProtobufTopic(NetworkTableInstance inst, int handle, Protobuf<T, ?> proto) {
super(inst, handle);
m_proto = proto;
}
/**
* Create a ProtobufTopic from a generic topic.
*
* @param <T> value class (inferred from proto)
* @param topic generic topic
* @param proto protobuf serialization implementation
* @return ProtobufTopic for value class
*/
public static <T> ProtobufTopic<T> wrap(Topic topic, Protobuf<T, ?> proto) {
return new ProtobufTopic<T>(topic, proto);
}
/**
* Create a ProtobufTopic from a native handle; generally NetworkTableInstance.getProtobufTopic()
* should be used instead.
*
* @param <T> value class (inferred from proto)
* @param inst Instance
* @param handle Native handle
* @param proto protobuf serialization implementation
* @return ProtobufTopic for value class
*/
public static <T> ProtobufTopic<T> wrap(
NetworkTableInstance inst, int handle, Protobuf<T, ?> proto) {
return new ProtobufTopic<T>(inst, handle, proto);
}
/**
* Create a new subscriber to the topic.
*
* <p>The subscriber is only active as long as the returned object is not closed.
*
* <p>Subscribers that do not match the published data type do not return any values. To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a getter function
* @param options subscribe options
* @return subscriber
*/
public ProtobufSubscriber<T> subscribe(T defaultValue, PubSubOption... options) {
return new ProtobufEntryImpl<T>(
this,
ProtobufBuffer.create(m_proto),
NetworkTablesJNI.subscribe(
m_handle, NetworkTableType.kRaw.getValue(), m_proto.getTypeString(), options),
defaultValue,
false);
}
/**
* Create a new publisher to the topic.
*
* <p>The publisher is only active as long as the returned object is not closed.
*
* <p>It is not possible to publish two different data types to the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored). To determine if
* the data type matches, use the appropriate Topic functions.
*
* @param options publish options
* @return publisher
*/
public ProtobufPublisher<T> publish(PubSubOption... options) {
m_inst.addSchema(m_proto);
return new ProtobufEntryImpl<T>(
this,
ProtobufBuffer.create(m_proto),
NetworkTablesJNI.publish(
m_handle, NetworkTableType.kRaw.getValue(), m_proto.getTypeString(), options),
null,
true);
}
/**
* Create a new publisher to the topic, with type string and initial properties.
*
* <p>The publisher is only active as long as the returned object is not closed.
*
* <p>It is not possible to publish two different data types to the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored). To determine if
* the data type matches, use the appropriate Topic functions.
*
* @param properties JSON properties
* @param options publish options
* @return publisher
* @throws IllegalArgumentException if properties is not a JSON object
*/
public ProtobufPublisher<T> publishEx(String properties, PubSubOption... options) {
m_inst.addSchema(m_proto);
return new ProtobufEntryImpl<T>(
this,
ProtobufBuffer.create(m_proto),
NetworkTablesJNI.publishEx(
m_handle,
NetworkTableType.kRaw.getValue(),
m_proto.getTypeString(),
properties,
options),
null,
true);
}
/**
* Create a new entry for the topic.
*
* <p>Entries act as a combination of a subscriber and a weak publisher. The subscriber is active
* as long as the entry is not closed. The publisher is created when the entry is first written
* to, and remains active until either unpublish() is called or the entry is closed.
*
* <p>It is not possible to use two different data types with the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored), and the entry
* will show no new values if the data type does not match. To determine if the data type matches,
* use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a getter function
* @param options publish and/or subscribe options
* @return entry
*/
public ProtobufEntry<T> getEntry(T defaultValue, PubSubOption... options) {
return new ProtobufEntryImpl<T>(
this,
ProtobufBuffer.create(m_proto),
NetworkTablesJNI.getEntry(
m_handle, NetworkTableType.kRaw.getValue(), m_proto.getTypeString(), options),
defaultValue,
false);
}
public Protobuf<T, ?> getProto() {
return m_proto;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof ProtobufTopic)) {
return false;
}
return super.equals(other) && m_proto == ((ProtobufTopic<?>) other).m_proto;
}
@Override
public int hashCode() {
return super.hashCode() ^ m_proto.hashCode();
}
private final Protobuf<T, ?> m_proto;
}

View File

@@ -0,0 +1,17 @@
// 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.networktables;
/**
* NetworkTables struct-encoded array value entry.
*
* <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
*
* @param <T> value class
*/
public interface StructArrayEntry<T> extends StructArraySubscriber<T>, StructArrayPublisher<T> {
/** Stops publishing the entry if it's published. */
void unpublish();
}

View File

@@ -0,0 +1,197 @@
// 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.networktables;
import edu.wpi.first.util.struct.StructBuffer;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
/**
* NetworkTables struct-encoded value implementation.
*
* @param <T> value class
*/
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
final class StructArrayEntryImpl<T> extends EntryBase implements StructArrayEntry<T> {
/**
* Constructor.
*
* @param topic Topic
* @param handle Native handle
* @param defaultValue Default value for get()
*/
StructArrayEntryImpl(
StructArrayTopic<T> topic,
StructBuffer<T> buf,
int handle,
T[] defaultValue,
boolean schemaPublished) {
super(handle);
m_topic = topic;
m_defaultValue = defaultValue;
m_buf = buf;
m_schemaPublished = schemaPublished;
}
@Override
public StructArrayTopic<T> getTopic() {
return m_topic;
}
@Override
public T[] get() {
return fromRaw(NetworkTablesJNI.getRaw(m_handle, m_emptyRaw), m_defaultValue);
}
@Override
public T[] get(T[] defaultValue) {
return fromRaw(NetworkTablesJNI.getRaw(m_handle, m_emptyRaw), defaultValue);
}
@Override
public TimestampedObject<T[]> getAtomic() {
return fromRaw(NetworkTablesJNI.getAtomicRaw(m_handle, m_emptyRaw), m_defaultValue);
}
@Override
public TimestampedObject<T[]> getAtomic(T[] defaultValue) {
return fromRaw(NetworkTablesJNI.getAtomicRaw(m_handle, m_emptyRaw), defaultValue);
}
@Override
public TimestampedObject<T[]>[] readQueue() {
TimestampedRaw[] raw = NetworkTablesJNI.readQueueRaw(m_handle);
@SuppressWarnings("unchecked")
TimestampedObject<T[]>[] arr = (TimestampedObject<T[]>[]) new TimestampedObject<?>[raw.length];
int err = 0;
for (int i = 0; i < raw.length; i++) {
arr[i] = fromRaw(raw[i], null);
if (arr[i].value == null) {
err++;
}
}
// discard bad values
if (err > 0) {
@SuppressWarnings("unchecked")
TimestampedObject<T[]>[] newArr =
(TimestampedObject<T[]>[]) new TimestampedObject<?>[raw.length - err];
int i = 0;
for (TimestampedObject<T[]> e : arr) {
if (e.value != null) {
arr[i] = e;
i++;
}
}
arr = newArr;
}
return arr;
}
@Override
public T[][] readQueueValues() {
byte[][] raw = NetworkTablesJNI.readQueueValuesRaw(m_handle);
@SuppressWarnings("unchecked")
T[][] arr = (T[][]) Array.newInstance(Array.class, raw.length);
int err = 0;
for (int i = 0; i < raw.length; i++) {
arr[i] = fromRaw(raw[i], null);
if (arr[i] == null) {
err++;
}
}
// discard bad values
if (err > 0) {
@SuppressWarnings("unchecked")
T[][] newArr = (T[][]) Array.newInstance(Array.class, raw.length - err);
int i = 0;
for (T[] e : arr) {
if (e != null) {
arr[i] = e;
i++;
}
}
arr = newArr;
}
return arr;
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
@Override
public void set(T[] value, long time) {
try {
synchronized (m_buf) {
if (!m_schemaPublished) {
m_schemaPublished = true;
m_topic.getInstance().addSchema(m_buf.getStruct());
}
ByteBuffer bb = m_buf.writeArray(value);
NetworkTablesJNI.setRaw(m_handle, time, bb, 0, bb.position());
}
} catch (RuntimeException e) {
// ignore
}
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
@Override
public void setDefault(T[] value) {
try {
synchronized (m_buf) {
if (!m_schemaPublished) {
m_schemaPublished = true;
m_topic.getInstance().addSchema(m_buf.getStruct());
}
ByteBuffer bb = m_buf.writeArray(value);
NetworkTablesJNI.setDefaultRaw(m_handle, 0, bb, 0, bb.position());
}
} catch (RuntimeException e) {
// ignore
}
}
@Override
public void unpublish() {
NetworkTablesJNI.unpublish(m_handle);
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
private T[] fromRaw(byte[] raw, T[] defaultValue) {
if (raw.length == 0) {
return defaultValue;
}
try {
synchronized (m_buf) {
return m_buf.readArray(raw);
}
} catch (RuntimeException e) {
return defaultValue;
}
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
private TimestampedObject<T[]> fromRaw(TimestampedRaw raw, T[] defaultValue) {
if (raw.value.length == 0) {
return new TimestampedObject<T[]>(0, 0, defaultValue);
}
try {
synchronized (m_buf) {
return new TimestampedObject<T[]>(
raw.timestamp, raw.serverTime, m_buf.readArray(raw.value));
}
} catch (RuntimeException e) {
return new TimestampedObject<T[]>(0, 0, defaultValue);
}
}
private final StructArrayTopic<T> m_topic;
private final T[] m_defaultValue;
private final StructBuffer<T> m_buf;
private boolean m_schemaPublished;
private static final byte[] m_emptyRaw = new byte[] {};
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.networktables;
import java.util.function.Consumer;
/**
* NetworkTables struct-encoded array value publisher.
*
* @param <T> value class
*/
public interface StructArrayPublisher<T> extends Publisher, Consumer<T[]> {
/**
* Get the corresponding topic.
*
* @return Topic
*/
@Override
StructArrayTopic<T> getTopic();
/**
* Publish a new value using current NT time.
*
* @param value value to publish
*/
default void set(T[] value) {
set(value, 0);
}
/**
* Publish a new value.
*
* @param value value to publish
* @param time timestamp; 0 indicates current NT time should be used
*/
void set(T[] value, long time);
/**
* Publish a default value. On reconnect, a default value will never be used in preference to a
* published value.
*
* @param value value
*/
void setDefault(T[] value);
@Override
default void accept(T[] value) {
set(value);
}
}

View File

@@ -0,0 +1,79 @@
// 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.networktables;
import java.util.function.Supplier;
/**
* NetworkTables struct-encoded array value subscriber.
*
* @param <T> value class
*/
@SuppressWarnings("PMD.MissingOverride")
public interface StructArraySubscriber<T> extends Subscriber, Supplier<T[]> {
/**
* Get the corresponding topic.
*
* @return Topic
*/
@Override
StructArrayTopic<T> getTopic();
/**
* Get the last published value. If no value has been published or the value cannot be unpacked,
* returns the stored default value.
*
* @return value
*/
T[] get();
/**
* Get the last published value. If no value has been published or the value cannot be unpacked,
* returns the passed defaultValue.
*
* @param defaultValue default value to return if no value has been published
* @return value
*/
T[] get(T[] defaultValue);
/**
* Get the last published value along with its timestamp. If no value has been published or the
* value cannot be unpacked, returns the stored default value and a timestamp of 0.
*
* @return timestamped value
*/
TimestampedObject<T[]> getAtomic();
/**
* Get the last published value along with its timestamp. If no value has been published or the
* value cannot be unpacked, returns the passed defaultValue and a timestamp of 0.
*
* @param defaultValue default value to return if no value has been published
* @return timestamped value
*/
TimestampedObject<T[]> getAtomic(T[] defaultValue);
/**
* Get an array of all valid value changes since the last call to readQueue. Also provides a
* timestamp for each value. Values that cannot be unpacked are dropped.
*
* <p>The "poll storage" subscribe option can be used to set the queue depth.
*
* @return Array of timestamped values; empty array if no valid new changes have been published
* since the previous call.
*/
TimestampedObject<T[]>[] readQueue();
/**
* Get an array of all valid value changes since the last call to readQueue. Values that cannot be
* unpacked are dropped.
*
* <p>The "poll storage" subscribe option can be used to set the queue depth.
*
* @return Array of values; empty array if no valid new changes have been published since the
* previous call.
*/
T[][] readQueueValues();
}

View File

@@ -0,0 +1,178 @@
// 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.networktables;
import edu.wpi.first.util.struct.Struct;
import edu.wpi.first.util.struct.StructBuffer;
/**
* NetworkTables struct-encoded array value topic.
*
* @param <T> value class
*/
public final class StructArrayTopic<T> extends Topic {
private StructArrayTopic(Topic topic, Struct<T> struct) {
super(topic.m_inst, topic.m_handle);
m_struct = struct;
}
private StructArrayTopic(NetworkTableInstance inst, int handle, Struct<T> struct) {
super(inst, handle);
m_struct = struct;
}
/**
* Create a StructArrayTopic from a generic topic.
*
* @param <T> value class (inferred from struct)
* @param topic generic topic
* @param struct struct serialization implementation
* @return StructArrayTopic for value class
*/
public static <T> StructArrayTopic<T> wrap(Topic topic, Struct<T> struct) {
return new StructArrayTopic<T>(topic, struct);
}
/**
* Create a StructArrayTopic from a native handle; generally
* NetworkTableInstance.getStructArrayTopic() should be used instead.
*
* @param <T> value class (inferred from struct)
* @param inst Instance
* @param handle Native handle
* @param struct struct serialization implementation
* @return StructArrayTopic for value class
*/
public static <T> StructArrayTopic<T> wrap(
NetworkTableInstance inst, int handle, Struct<T> struct) {
return new StructArrayTopic<T>(inst, handle, struct);
}
/**
* Create a new subscriber to the topic.
*
* <p>The subscriber is only active as long as the returned object is not closed.
*
* <p>Subscribers that do not match the published data type do not return any values. To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a getter function
* @param options subscribe options
* @return subscriber
*/
public StructArraySubscriber<T> subscribe(T[] defaultValue, PubSubOption... options) {
return new StructArrayEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.subscribe(
m_handle, NetworkTableType.kRaw.getValue(), m_struct.getTypeString() + "[]", options),
defaultValue,
false);
}
/**
* Create a new publisher to the topic.
*
* <p>The publisher is only active as long as the returned object is not closed.
*
* <p>It is not possible to publish two different data types to the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored). To determine if
* the data type matches, use the appropriate Topic functions.
*
* @param options publish options
* @return publisher
*/
public StructArrayPublisher<T> publish(PubSubOption... options) {
m_inst.addSchema(m_struct);
return new StructArrayEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.publish(
m_handle, NetworkTableType.kRaw.getValue(), m_struct.getTypeString() + "[]", options),
null,
true);
}
/**
* Create a new publisher to the topic, with type string and initial properties.
*
* <p>The publisher is only active as long as the returned object is not closed.
*
* <p>It is not possible to publish two different data types to the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored). To determine if
* the data type matches, use the appropriate Topic functions.
*
* @param properties JSON properties
* @param options publish options
* @return publisher
* @throws IllegalArgumentException if properties is not a JSON object
*/
public StructArrayPublisher<T> publishEx(String properties, PubSubOption... options) {
m_inst.addSchema(m_struct);
return new StructArrayEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.publishEx(
m_handle,
NetworkTableType.kRaw.getValue(),
m_struct.getTypeString() + "[]",
properties,
options),
null,
true);
}
/**
* Create a new entry for the topic.
*
* <p>Entries act as a combination of a subscriber and a weak publisher. The subscriber is active
* as long as the entry is not closed. The publisher is created when the entry is first written
* to, and remains active until either unpublish() is called or the entry is closed.
*
* <p>It is not possible to use two different data types with the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored), and the entry
* will show no new values if the data type does not match. To determine if the data type matches,
* use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a getter function
* @param options publish and/or subscribe options
* @return entry
*/
public StructArrayEntry<T> getEntry(T[] defaultValue, PubSubOption... options) {
return new StructArrayEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.getEntry(
m_handle, NetworkTableType.kRaw.getValue(), m_struct.getTypeString() + "[]", options),
defaultValue,
false);
}
public Struct<T> getStruct() {
return m_struct;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof StructArrayTopic)) {
return false;
}
return super.equals(other) && m_struct == ((StructArrayTopic<?>) other).m_struct;
}
@Override
public int hashCode() {
return super.hashCode() ^ m_struct.hashCode();
}
private final Struct<T> m_struct;
}

View File

@@ -0,0 +1,17 @@
// 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.networktables;
/**
* NetworkTables struct-encoded value entry.
*
* <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
*
* @param <T> value class
*/
public interface StructEntry<T> extends StructSubscriber<T>, StructPublisher<T> {
/** Stops publishing the entry if it's published. */
void unpublish();
}

View File

@@ -0,0 +1,207 @@
// 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.networktables;
import edu.wpi.first.util.struct.StructBuffer;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
/**
* NetworkTables struct-encoded value implementation.
*
* @param <T> value class
*/
final class StructEntryImpl<T> extends EntryBase implements StructEntry<T> {
/**
* Constructor.
*
* @param topic Topic
* @param handle Native handle
* @param defaultValue Default value for get()
*/
StructEntryImpl(
StructTopic<T> topic,
StructBuffer<T> buf,
int handle,
T defaultValue,
boolean schemaPublished) {
super(handle);
m_topic = topic;
m_defaultValue = defaultValue;
m_buf = buf;
m_schemaPublished = schemaPublished;
}
@Override
public StructTopic<T> getTopic() {
return m_topic;
}
@Override
public T get() {
return fromRaw(NetworkTablesJNI.getRaw(m_handle, m_emptyRaw), m_defaultValue);
}
@Override
public T get(T defaultValue) {
return fromRaw(NetworkTablesJNI.getRaw(m_handle, m_emptyRaw), defaultValue);
}
@Override
public boolean getInto(T out) {
byte[] raw = NetworkTablesJNI.getRaw(m_handle, m_emptyRaw);
if (raw.length == 0) {
return false;
}
synchronized (m_buf) {
m_buf.readInto(out, raw);
return true;
}
}
@Override
public TimestampedObject<T> getAtomic() {
return fromRaw(NetworkTablesJNI.getAtomicRaw(m_handle, m_emptyRaw), m_defaultValue);
}
@Override
public TimestampedObject<T> getAtomic(T defaultValue) {
return fromRaw(NetworkTablesJNI.getAtomicRaw(m_handle, m_emptyRaw), defaultValue);
}
@Override
public TimestampedObject<T>[] readQueue() {
TimestampedRaw[] raw = NetworkTablesJNI.readQueueRaw(m_handle);
@SuppressWarnings("unchecked")
TimestampedObject<T>[] arr = (TimestampedObject<T>[]) new TimestampedObject<?>[raw.length];
int err = 0;
for (int i = 0; i < raw.length; i++) {
arr[i] = fromRaw(raw[i], null);
if (arr[i].value == null) {
err++;
}
}
// discard bad values
if (err > 0) {
@SuppressWarnings("unchecked")
TimestampedObject<T>[] newArr =
(TimestampedObject<T>[]) new TimestampedObject<?>[raw.length - err];
int i = 0;
for (TimestampedObject<T> e : arr) {
if (e.value != null) {
arr[i] = e;
i++;
}
}
arr = newArr;
}
return arr;
}
@Override
public T[] readQueueValues() {
byte[][] raw = NetworkTablesJNI.readQueueValuesRaw(m_handle);
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(m_topic.getStruct().getTypeClass(), raw.length);
int err = 0;
for (int i = 0; i < raw.length; i++) {
arr[i] = fromRaw(raw[i], null);
if (arr[i] == null) {
err++;
}
}
// discard bad values
if (err > 0) {
@SuppressWarnings("unchecked")
T[] newArr = (T[]) Array.newInstance(m_topic.getStruct().getTypeClass(), raw.length - err);
int i = 0;
for (T e : arr) {
if (e != null) {
arr[i] = e;
i++;
}
}
arr = newArr;
}
return arr;
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
@Override
public void set(T value, long time) {
try {
synchronized (m_buf) {
if (!m_schemaPublished) {
m_schemaPublished = true;
m_topic.getInstance().addSchema(m_buf.getStruct());
}
ByteBuffer bb = m_buf.write(value);
NetworkTablesJNI.setRaw(m_handle, time, bb, 0, bb.position());
}
} catch (RuntimeException e) {
// ignore
}
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
@Override
public void setDefault(T value) {
try {
synchronized (m_buf) {
if (!m_schemaPublished) {
m_schemaPublished = true;
m_topic.getInstance().addSchema(m_buf.getStruct());
}
ByteBuffer bb = m_buf.write(value);
NetworkTablesJNI.setDefaultRaw(m_handle, 0, bb, 0, bb.position());
}
} catch (RuntimeException e) {
// ignore
}
}
@Override
public void unpublish() {
NetworkTablesJNI.unpublish(m_handle);
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
private T fromRaw(byte[] raw, T defaultValue) {
if (raw.length == 0) {
return defaultValue;
}
try {
synchronized (m_buf) {
return m_buf.read(raw);
}
} catch (RuntimeException e) {
return defaultValue;
}
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
private TimestampedObject<T> fromRaw(TimestampedRaw raw, T defaultValue) {
if (raw.value.length == 0) {
return new TimestampedObject<T>(0, 0, defaultValue);
}
try {
synchronized (m_buf) {
return new TimestampedObject<T>(raw.timestamp, raw.serverTime, m_buf.read(raw.value));
}
} catch (RuntimeException e) {
return new TimestampedObject<T>(0, 0, defaultValue);
}
}
private final StructTopic<T> m_topic;
private final T m_defaultValue;
private final StructBuffer<T> m_buf;
private boolean m_schemaPublished;
private static final byte[] m_emptyRaw = new byte[] {};
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.networktables;
import java.util.function.Consumer;
/**
* NetworkTables struct-encoded value publisher.
*
* @param <T> value class
*/
public interface StructPublisher<T> extends Publisher, Consumer<T> {
/**
* Get the corresponding topic.
*
* @return Topic
*/
@Override
StructTopic<T> getTopic();
/**
* Publish a new value using current NT time.
*
* @param value value to publish
*/
default void set(T value) {
set(value, 0);
}
/**
* Publish a new value.
*
* @param value value to publish
* @param time timestamp; 0 indicates current NT time should be used
*/
void set(T value, long time);
/**
* Publish a default value. On reconnect, a default value will never be used in preference to a
* published value.
*
* @param value value
*/
void setDefault(T value);
@Override
default void accept(T value) {
set(value);
}
}

View File

@@ -0,0 +1,94 @@
// 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.networktables;
import java.util.function.Supplier;
/**
* NetworkTables struct-encoded value subscriber.
*
* @param <T> value class
*/
@SuppressWarnings("PMD.MissingOverride")
public interface StructSubscriber<T> extends Subscriber, Supplier<T> {
/**
* Get the corresponding topic.
*
* @return Topic
*/
@Override
StructTopic<T> getTopic();
/**
* Get the last published value. If no value has been published or the value cannot be unpacked,
* returns the stored default value.
*
* @return value
*/
T get();
/**
* Get the last published value. If no value has been published or the value cannot be unpacked,
* returns the passed defaultValue.
*
* @param defaultValue default value to return if no value has been published
* @return value
*/
T get(T defaultValue);
/**
* Get the last published value, replacing the contents in place of an existing object. If no
* value has been published or the value cannot be unpacked, does not replace the contents and
* returns false. This function will not work (will throw UnsupportedOperationException) unless T
* is mutable (and the implementation of Struct implements unpackInto).
*
* <p>Note: due to Java language limitations, it's not possible to validate at compile time that
* the out parameter is mutable.
*
* @param out object to replace contents of; must be mutable
* @return true if successful, false if no value has been published
* @throws UnsupportedOperationException if T is immutable
*/
boolean getInto(T out);
/**
* Get the last published value along with its timestamp. If no value has been published or the
* value cannot be unpacked, returns the stored default value and a timestamp of 0.
*
* @return timestamped value
*/
TimestampedObject<T> getAtomic();
/**
* Get the last published value along with its timestamp If no value has been published or the
* value cannot be unpacked, returns the passed defaultValue and a timestamp of 0.
*
* @param defaultValue default value to return if no value has been published
* @return timestamped value
*/
TimestampedObject<T> getAtomic(T defaultValue);
/**
* Get an array of all valid value changes since the last call to readQueue. Also provides a
* timestamp for each value. Values that cannot be unpacked are dropped.
*
* <p>The "poll storage" subscribe option can be used to set the queue depth.
*
* @return Array of timestamped values; empty array if no valid new changes have been published
* since the previous call.
*/
TimestampedObject<T>[] readQueue();
/**
* Get an array of all value changes since the last call to readQueue. Values that cannot be
* unpacked are dropped.
*
* <p>The "poll storage" subscribe option can be used to set the queue depth.
*
* @return Array of values; empty array if no valid new changes have been published since the
* previous call.
*/
T[] readQueueValues();
}

View File

@@ -0,0 +1,177 @@
// 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.networktables;
import edu.wpi.first.util.struct.Struct;
import edu.wpi.first.util.struct.StructBuffer;
/**
* NetworkTables struct-encoded value topic.
*
* @param <T> value class
*/
public final class StructTopic<T> extends Topic {
private StructTopic(Topic topic, Struct<T> struct) {
super(topic.m_inst, topic.m_handle);
m_struct = struct;
}
private StructTopic(NetworkTableInstance inst, int handle, Struct<T> struct) {
super(inst, handle);
m_struct = struct;
}
/**
* Create a StructTopic from a generic topic.
*
* @param <T> value class (inferred from struct)
* @param topic generic topic
* @param struct struct serialization implementation
* @return StructTopic for value class
*/
public static <T> StructTopic<T> wrap(Topic topic, Struct<T> struct) {
return new StructTopic<T>(topic, struct);
}
/**
* Create a StructTopic from a native handle; generally NetworkTableInstance.getStructTopic()
* should be used instead.
*
* @param <T> value class (inferred from struct)
* @param inst Instance
* @param handle Native handle
* @param struct struct serialization implementation
* @return StructTopic for value class
*/
public static <T> StructTopic<T> wrap(NetworkTableInstance inst, int handle, Struct<T> struct) {
return new StructTopic<T>(inst, handle, struct);
}
/**
* Create a new subscriber to the topic.
*
* <p>The subscriber is only active as long as the returned object is not closed.
*
* <p>Subscribers that do not match the published data type do not return any values. To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a getter function
* @param options subscribe options
* @return subscriber
*/
public StructSubscriber<T> subscribe(T defaultValue, PubSubOption... options) {
return new StructEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.subscribe(
m_handle, NetworkTableType.kRaw.getValue(), m_struct.getTypeString(), options),
defaultValue,
false);
}
/**
* Create a new publisher to the topic.
*
* <p>The publisher is only active as long as the returned object is not closed.
*
* <p>It is not possible to publish two different data types to the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored). To determine if
* the data type matches, use the appropriate Topic functions.
*
* @param options publish options
* @return publisher
*/
public StructPublisher<T> publish(PubSubOption... options) {
m_inst.addSchema(m_struct);
return new StructEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.publish(
m_handle, NetworkTableType.kRaw.getValue(), m_struct.getTypeString(), options),
null,
true);
}
/**
* Create a new publisher to the topic, with type string and initial properties.
*
* <p>The publisher is only active as long as the returned object is not closed.
*
* <p>It is not possible to publish two different data types to the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored). To determine if
* the data type matches, use the appropriate Topic functions.
*
* @param properties JSON properties
* @param options publish options
* @return publisher
* @throws IllegalArgumentException if properties is not a JSON object
*/
public StructPublisher<T> publishEx(String properties, PubSubOption... options) {
m_inst.addSchema(m_struct);
return new StructEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.publishEx(
m_handle,
NetworkTableType.kRaw.getValue(),
m_struct.getTypeString(),
properties,
options),
null,
true);
}
/**
* Create a new entry for the topic.
*
* <p>Entries act as a combination of a subscriber and a weak publisher. The subscriber is active
* as long as the entry is not closed. The publisher is created when the entry is first written
* to, and remains active until either unpublish() is called or the entry is closed.
*
* <p>It is not possible to use two different data types with the same topic. Conflicts between
* publishers are typically resolved by the server on a first-come, first-served basis. Any
* published values that do not match the topic's data type are dropped (ignored), and the entry
* will show no new values if the data type does not match. To determine if the data type matches,
* use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a getter function
* @param options publish and/or subscribe options
* @return entry
*/
public StructEntry<T> getEntry(T defaultValue, PubSubOption... options) {
return new StructEntryImpl<T>(
this,
StructBuffer.create(m_struct),
NetworkTablesJNI.getEntry(
m_handle, NetworkTableType.kRaw.getValue(), m_struct.getTypeString(), options),
defaultValue,
false);
}
public Struct<T> getStruct() {
return m_struct;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof StructTopic)) {
return false;
}
return super.equals(other) && m_struct == ((StructTopic<?>) other).m_struct;
}
@Override
public int hashCode() {
return super.hashCode() ^ m_struct.hashCode();
}
private final Struct<T> m_struct;
}

View File

@@ -0,0 +1,33 @@
// 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.networktables;
/** NetworkTables timestamped object. */
public final class TimestampedObject<T> {
/**
* Create a timestamped value.
*
* @param timestamp timestamp in local time base
* @param serverTime timestamp in server time base
* @param value value
*/
public TimestampedObject(long timestamp, long serverTime, T value) {
this.timestamp = timestamp;
this.serverTime = serverTime;
this.value = value;
}
/** Timestamp in local time base. */
@SuppressWarnings("MemberName")
public final long timestamp;
/** Timestamp in server time base. May be 0 or 1 for locally set values. */
@SuppressWarnings("MemberName")
public final long serverTime;
/** Value. */
@SuppressWarnings("MemberName")
public final T value;
}

View File

@@ -5,7 +5,7 @@
#include "ConnectionList.h"
#include <wpi/SmallVector.h>
#include <wpi/json_serializer.h>
#include <wpi/json.h>
#include <wpi/raw_ostream.h>
#include "IListenerStorage.h"

View File

@@ -7,6 +7,7 @@
#include <algorithm>
#include <wpi/DataLog.h>
#include <wpi/SmallString.h>
#include <wpi/StringExtras.h>
#include <wpi/json.h>
@@ -857,14 +858,14 @@ LocalStorage::PublisherData* LocalStorage::Impl::PublishEntry(EntryData* entry,
if (entry->publisher) {
return entry->publisher;
}
auto typeStr = TypeToString(type);
if (entry->subscriber->config.type == NT_UNASSIGNED) {
auto typeStr = TypeToString(type);
entry->subscriber->config.type = type;
entry->subscriber->config.typeStr = typeStr;
} else if (entry->subscriber->config.type != type ||
entry->subscriber->config.typeStr != typeStr) {
} else if (entry->subscriber->config.type != type) {
if (!IsNumericCompatible(type, entry->subscriber->config.type)) {
// don't allow dynamically changing the type of an entry
auto typeStr = TypeToString(type);
ERR("cannot publish entry {} as type {}, previously subscribed as {}",
entry->topic->name, typeStr, entry->subscriber->config.typeStr);
return nullptr;
@@ -1481,6 +1482,41 @@ void LocalStorage::StopDataLog(NT_DataLogger logger) {
}
}
bool LocalStorage::HasSchema(std::string_view name) {
std::scoped_lock lock{m_mutex};
wpi::SmallString<128> fullName{"/.schema/"};
fullName += name;
auto it = m_impl.m_schemas.find(fullName);
return it != m_impl.m_schemas.end();
}
void LocalStorage::AddSchema(std::string_view name, std::string_view type,
std::span<const uint8_t> schema) {
std::scoped_lock lock{m_mutex};
wpi::SmallString<128> fullName{"/.schema/"};
fullName += name;
auto& pubHandle = m_impl.m_schemas[fullName];
if (pubHandle != 0) {
return;
}
auto topic = m_impl.GetOrCreateTopic(fullName);
if (topic->localPublishers.size() >= kMaxPublishers) {
WPI_ERROR(m_impl.m_logger,
"reached maximum number of publishers to '{}', not publishing",
topic->name);
return;
}
pubHandle = m_impl
.AddLocalPublisher(topic, {{"retained", true}},
PubSubConfig{NT_RAW, type, {}})
->handle;
m_impl.SetDefaultEntryValue(pubHandle, Value::MakeRaw(schema));
}
void LocalStorage::Reset() {
std::scoped_lock lock{m_mutex};
m_impl.m_network = nullptr;

View File

@@ -321,6 +321,13 @@ class LocalStorage final : public net::ILocalStorage {
std::string_view logPrefix);
void StopDataLog(NT_DataLogger logger);
//
// Schema functions
//
bool HasSchema(std::string_view name);
void AddSchema(std::string_view name, std::string_view type,
std::span<const uint8_t> schema);
void Reset();
private:
@@ -549,6 +556,9 @@ class LocalStorage final : public net::ILocalStorage {
// string-based listeners
VectorSet<ListenerData*> m_topicPrefixListeners;
// schema publishers
wpi::StringMap<NT_Publisher> m_schemas;
// topic functions
void NotifyTopic(TopicData* topic, unsigned int eventFlags);

View File

@@ -242,7 +242,11 @@ void NetworkClient3::TcpConnected(uv::Tcp& tcp) {
tcp.error.connect([this, &tcp](uv::Error err) {
DEBUG3("NT3 TCP error {}", err.str());
if (!tcp.IsLoopClosing()) {
DoDisconnect(err.str());
// we could be in the middle of sending data, so defer disconnect
uv::Timer::SingleShot(m_loop, uv::Timer::Time{0},
[this, reason = std::string{err.str()}] {
DoDisconnect(reason);
});
}
});
tcp.end.connect([this, &tcp] {
@@ -397,6 +401,7 @@ void NetworkClient::WsConnected(wpi::WebSocket& ws, uv::Tcp& tcp,
m_wire =
std::make_shared<net::WebSocketConnection>(ws, connInfo.protocol_version);
m_wire->Start();
m_clientImpl = std::make_unique<net::ClientImpl>(
m_loop.Now().count(), m_inst, *m_wire, m_logger, m_timeSyncUpdated,
[this](uint32_t repeatMs) {
@@ -412,7 +417,14 @@ void NetworkClient::WsConnected(wpi::WebSocket& ws, uv::Tcp& tcp,
m_clientImpl->SendInitial();
ws.closed.connect([this, &ws](uint16_t, std::string_view reason) {
if (!ws.GetStream().IsLoopClosing()) {
DoDisconnect(reason);
// we could be in the middle of sending data, so defer disconnect
// capture a shared_ptr copy of ws to make sure it doesn't get destroyed
// until after DoDisconnect returns
uv::Timer::SingleShot(
m_loop, uv::Timer::Time{0},
[this, reason = std::string{reason}, keepws = ws.shared_from_this()] {
DoDisconnect(reason);
});
}
});
ws.text.connect([this](std::string_view data, bool) {

View File

@@ -280,6 +280,7 @@ void NetworkServer::ServerConnection4::ProcessWsUpgrade() {
INFO("CONNECTED NT4 client '{}' (from {})", dedupName, m_connInfo);
m_info.remote_id = dedupName;
m_server.AddConnection(this, m_info);
m_wire->Start();
m_websocket->closed.connect([this](uint16_t, std::string_view reason) {
auto realReason = m_wire->GetDisconnectReason();
INFO("DISCONNECTED NT4 client '{}' (from {}): {}", m_info.remote_id,

View File

@@ -719,7 +719,7 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_setTopicProperty
{
wpi::json j;
try {
j = wpi::json::parse(JStringRef{env, value});
j = wpi::json::parse(std::string_view{JStringRef{env, value}});
} catch (wpi::json::parse_error& err) {
illegalArgEx.Throw(
env, fmt::format("could not parse value JSON: {}", err.what()));
@@ -763,7 +763,7 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_setTopicProperties
{
wpi::json j;
try {
j = wpi::json::parse(JStringRef{env, properties});
j = wpi::json::parse(std::string_view{JStringRef{env, properties}});
} catch (wpi::json::parse_error& err) {
illegalArgEx.Throw(
env, fmt::format("could not parse properties JSON: {}", err.what()));
@@ -828,7 +828,7 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_publishEx
{
wpi::json j;
try {
j = wpi::json::parse(JStringRef{env, properties});
j = wpi::json::parse(std::string_view{JStringRef{env, properties}});
} catch (wpi::json::parse_error& err) {
illegalArgEx.Throw(
env, fmt::format("could not parse properties JSON: {}", err.what()));

View File

@@ -280,9 +280,15 @@ void NetworkOutgoingQueue<MessageType>::SendOutgoing(uint64_t curTimeMs,
});
}
}
if (unsent < 0) {
return; // error
}
if (unsent == 0) {
// finish writing any partial buffers
unsent = m_wire.Flush();
if (unsent < 0) {
return; // error
}
}
int delta = it - msgs.begin() - unsent;
for (auto&& msg : std::span{msgs}.subspan(0, delta)) {

View File

@@ -17,7 +17,7 @@
#include <wpi/MessagePack.h>
#include <wpi/SmallVector.h>
#include <wpi/StringExtras.h>
#include <wpi/json_serializer.h>
#include <wpi/json.h>
#include <wpi/raw_ostream.h>
#include <wpi/timestamp.h>
@@ -524,6 +524,9 @@ void ServerImpl::ClientData4::SendAnnounce(TopicData* topic,
WireEncodeAnnounce(os, topic->name, topic->id, topic->typeStr,
topic->properties, pubuid);
});
if (unsent < 0) {
return; // error
}
if (unsent == 0 && m_wire.Flush() == 0) {
return;
}
@@ -544,6 +547,9 @@ void ServerImpl::ClientData4::SendUnannounce(TopicData* topic) {
if (m_local) {
int unsent = m_wire.WriteText(
[&](auto& os) { WireEncodeUnannounce(os, topic->name, topic->id); });
if (unsent < 0) {
return; // error
}
if (unsent == 0 && m_wire.Flush() == 0) {
return;
}
@@ -565,6 +571,9 @@ void ServerImpl::ClientData4::SendPropertiesUpdate(TopicData* topic,
int unsent = m_wire.WriteText([&](auto& os) {
WireEncodePropertiesUpdate(os, topic->name, update, ack);
});
if (unsent < 0) {
return; // error
}
if (unsent == 0 && m_wire.Flush() == 0) {
return;
}
@@ -1878,12 +1887,16 @@ void ServerImpl::SetLocal(LocalInterface* local) {
}
void ServerImpl::ProcessIncomingText(int clientId, std::string_view data) {
m_clients[clientId]->ProcessIncomingText(data);
if (auto client = m_clients[clientId].get()) {
client->ProcessIncomingText(data);
}
}
void ServerImpl::ProcessIncomingBinary(int clientId,
std::span<const uint8_t> data) {
m_clients[clientId]->ProcessIncomingBinary(data);
if (auto client = m_clients[clientId].get()) {
client->ProcessIncomingBinary(data);
}
}
void ServerImpl::ConnectionsChanged(const std::vector<ConnectionInfo>& conns) {

View File

@@ -4,6 +4,7 @@
#include "WebSocketConnection.h"
#include <algorithm>
#include <span>
#include <wpi/Endian.h>
@@ -30,7 +31,8 @@ class WebSocketConnection::Stream final : public wpi::raw_ostream {
public:
explicit Stream(WebSocketConnection& conn) : m_conn{conn} {
auto& buf = conn.m_bufs.back();
SetBuffer(buf.base + buf.len, kAllocSize - buf.len);
SetBuffer(buf.base, kAllocSize);
SetNumBytesInBuffer(buf.len);
}
~Stream() final {
@@ -48,51 +50,47 @@ class WebSocketConnection::Stream final : public wpi::raw_ostream {
};
void WebSocketConnection::Stream::write_impl(const char* data, size_t len) {
if (len > kAllocSize) {
// only called by raw_ostream::write() when the buffer is empty and a large
// thing is being written; called with a length that's a multiple of the
// alloc size
assert((len % kAllocSize) == 0);
assert(m_conn.m_bufs.back().len == 0);
while (len > 0) {
auto& buf = m_conn.m_bufs.back();
std::memcpy(buf.base, data, kAllocSize);
buf.len = kAllocSize;
m_conn.m_framePos += kAllocSize;
m_conn.m_written += kAllocSize;
data += kAllocSize;
len -= kAllocSize;
if (data == m_conn.m_bufs.back().base) {
// flush_nonempty() case
m_conn.m_bufs.back().len = len;
if (!m_disableAlloc) {
m_conn.m_frames.back().opcode &= ~wpi::WebSocket::kFlagFin;
m_conn.StartFrame(wpi::WebSocket::Frame::kFragment);
SetBuffer(m_conn.m_bufs.back().base, kAllocSize);
}
return;
}
bool updateBuffer = false;
while (len > 0) {
auto& buf = m_conn.m_bufs.back();
assert(buf.len <= kAllocSize);
size_t amt = (std::min)(static_cast<int>(kAllocSize - buf.len),
static_cast<int>(len));
if (amt > 0) {
std::memcpy(buf.base + buf.len, data, amt);
buf.len += amt;
m_conn.m_framePos += amt;
m_conn.m_written += amt;
data += amt;
len -= amt;
}
if (buf.len >= kAllocSize && (len > 0 || !m_disableAlloc)) {
// fragment the current frame and start a new one
m_conn.m_frames.back().opcode &= ~wpi::WebSocket::kFlagFin;
m_conn.StartFrame(wpi::WebSocket::Frame::kFragment);
updateBuffer = true;
}
SetBuffer(m_conn.m_bufs.back().base, kAllocSize);
[[unlikely]] return;
}
auto& buf = m_conn.m_bufs.back();
buf.len += len;
m_conn.m_framePos += len;
m_conn.m_written += len;
if (!m_disableAlloc && buf.len >= kAllocSize) {
// fragment the current frame and start a new one
[[unlikely]] m_conn.m_frames.back().opcode &= ~wpi::WebSocket::kFlagFin;
m_conn.StartFrame(wpi::WebSocket::Frame::kFragment);
if (updateBuffer) {
SetBuffer(m_conn.m_bufs.back().base, kAllocSize);
}
}
WebSocketConnection::WebSocketConnection(wpi::WebSocket& ws,
unsigned int version)
: m_ws{ws}, m_version{version} {
m_ws.pong.connect([this](auto data) {
if (data.size() != 8) {
return;
}
m_lastPingResponse =
wpi::support::endian::read64<wpi::support::native>(data.data());
});
}
: m_ws{ws}, m_version{version} {}
WebSocketConnection::~WebSocketConnection() {
for (auto&& buf : m_bufs) {
@@ -103,6 +101,18 @@ WebSocketConnection::~WebSocketConnection() {
}
}
void WebSocketConnection::Start() {
m_ws.pong.connect([selfweak = weak_from_this()](auto data) {
if (data.size() != 8) {
return;
}
if (auto self = selfweak.lock()) {
self->m_lastPingResponse =
wpi::support::endian::read64<wpi::support::native>(data.data());
}
});
}
void WebSocketConnection::SendPing(uint64_t time) {
auto buf = AllocBuf();
buf.len = 8;
@@ -128,7 +138,7 @@ void WebSocketConnection::StartFrame(uint8_t opcode) {
void WebSocketConnection::FinishText() {
assert(!m_bufs.empty());
auto& buf = m_bufs.back();
assert(buf.len < kAllocSize + 1); // safe because we alloc one more byte
assert(buf.len < (kAllocSize + 1)); // safe because we alloc one more byte
buf.base[buf.len++] = ']';
}
@@ -186,7 +196,8 @@ int WebSocketConnection::Flush() {
m_ws_frames.reserve(m_frames.size());
for (auto&& frame : m_frames) {
m_ws_frames.emplace_back(
frame.opcode, std::span{&m_bufs[frame.start], &m_bufs[frame.end]});
frame.opcode,
std::span{m_bufs}.subspan(frame.start, frame.end - frame.start));
}
auto unsentFrames = m_ws.TrySendFrames(
@@ -255,8 +266,12 @@ wpi::uv::Buffer WebSocketConnection::AllocBuf() {
}
void WebSocketConnection::ReleaseBufs(std::span<wpi::uv::Buffer> bufs) {
#ifdef __SANITIZE_ADDRESS__
size_t numToPool = 0;
#else
size_t numToPool = (std::min)(bufs.size(), kMaxPoolSize - m_buf_pool.size());
m_buf_pool.insert(m_buf_pool.end(), bufs.begin(), bufs.begin() + numToPool);
#endif
for (auto&& buf : bufs.subspan(numToPool)) {
buf.Deallocate();
}

View File

@@ -26,6 +26,8 @@ class WebSocketConnection final
WebSocketConnection(const WebSocketConnection&) = delete;
WebSocketConnection& operator=(const WebSocketConnection&) = delete;
void Start();
unsigned int GetVersion() const final { return m_version; }
void SendPing(uint64_t time) final;

View File

@@ -6,7 +6,7 @@
#include <optional>
#include <wpi/json_serializer.h>
#include <wpi/json.h>
#include <wpi/mpack.h>
#include <wpi/raw_ostream.h>

View File

@@ -28,10 +28,14 @@ void UvStreamConnection3::Flush() {
++m_sendsActive;
m_stream.Write(m_buffers, [selfweak = weak_from_this()](auto bufs, auto) {
if (auto self = selfweak.lock()) {
#ifdef __SANITIZE_ADDRESS__
size_t numToPool = 0;
#else
size_t numToPool =
(std::min)(bufs.size(), kMaxPoolSize - self->m_buf_pool.size());
self->m_buf_pool.insert(self->m_buf_pool.end(), bufs.begin(),
bufs.begin() + numToPool);
#endif
for (auto&& buf : bufs.subspan(numToPool)) {
buf.Deallocate();
}

View File

@@ -7,9 +7,14 @@
#include <wpi/json.h>
#include "networktables/GenericEntry.h"
#include "networktables/NetworkTableInstance.h"
using namespace nt;
NetworkTableInstance Topic::GetInstance() const {
return NetworkTableInstance{GetInstanceFromHandle(m_handle)};
}
wpi::json Topic::GetProperty(std::string_view name) const {
return ::nt::GetTopicProperty(m_handle, name);
}

View File

@@ -625,6 +625,15 @@ NT_Listener NT_AddPolledLogger(NT_ListenerPoller poller, unsigned int min_level,
return nt::AddPolledLogger(poller, min_level, max_level);
}
NT_Bool NT_HasSchema(NT_Inst inst, const char* name) {
return nt::HasSchema(inst, name);
}
void NT_AddSchema(NT_Inst inst, const char* name, const char* type,
const uint8_t* schema, size_t schemaSize) {
nt::AddSchema(inst, name, type, {schema, schemaSize});
}
void NT_DisposeValue(NT_Value* value) {
switch (value->type) {
case NT_UNASSIGNED:

View File

@@ -782,4 +782,19 @@ NT_Listener AddPolledLogger(NT_ListenerPoller poller, unsigned int minLevel,
}
}
bool HasSchema(NT_Inst inst, std::string_view name) {
if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
return ii->localStorage.HasSchema(name);
} else {
return false;
}
}
void AddSchema(NT_Inst inst, std::string_view name, std::string_view type,
std::span<const uint8_t> schema) {
if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
ii->localStorage.AddSchema(name, type, schema);
}
}
} // namespace nt

View File

@@ -14,8 +14,11 @@
#include <wpi/StringMap.h>
#include <wpi/mutex.h>
#include <wpi/protobuf/Protobuf.h>
#include <wpi/struct/Struct.h>
#include "networktables/NetworkTableEntry.h"
#include "networktables/Topic.h"
#include "ntcore_c.h"
namespace nt {
@@ -29,9 +32,15 @@ class FloatTopic;
class IntegerArrayTopic;
class IntegerTopic;
class NetworkTableInstance;
template <wpi::ProtobufSerializable T>
class ProtobufTopic;
class RawTopic;
class StringArrayTopic;
class StringTopic;
template <wpi::StructSerializable T>
class StructArrayTopic;
template <wpi::StructSerializable T>
class StructTopic;
class Topic;
/**
@@ -220,6 +229,39 @@ class NetworkTable final {
*/
StringArrayTopic GetStringArrayTopic(std::string_view name) const;
/**
* Gets a protobuf serialized value topic.
*
* @param name topic name
* @return Topic
*/
template <wpi::ProtobufSerializable T>
ProtobufTopic<T> GetProtobufTopic(std::string_view name) const {
return ProtobufTopic<T>{GetTopic(name)};
}
/**
* Gets a raw struct serialized value topic.
*
* @param name topic name
* @return Topic
*/
template <wpi::StructSerializable T>
StructTopic<T> GetStructTopic(std::string_view name) const {
return StructTopic<T>{GetTopic(name)};
}
/**
* Gets a raw struct serialized array topic.
*
* @param name topic name
* @return Topic
*/
template <wpi::StructSerializable T>
StructArrayTopic<T> GetStructArrayTopic(std::string_view name) const {
return StructArrayTopic<T>{GetTopic(name)};
}
/**
* Returns the table at the specified key. If there is no table at the
* specified key, it will create a new table

View File

@@ -13,6 +13,9 @@
#include <utility>
#include <vector>
#include <wpi/protobuf/Protobuf.h>
#include <wpi/struct/Struct.h>
#include "networktables/NetworkTable.h"
#include "networktables/NetworkTableEntry.h"
#include "ntcore_c.h"
@@ -29,9 +32,15 @@ class FloatTopic;
class IntegerArrayTopic;
class IntegerTopic;
class MultiSubscriber;
template <wpi::ProtobufSerializable T>
class ProtobufTopic;
class RawTopic;
class StringArrayTopic;
class StringTopic;
template <wpi::StructSerializable T>
class StructArrayTopic;
template <wpi::StructSerializable T>
class StructTopic;
class Subscriber;
class Topic;
@@ -238,6 +247,33 @@ class NetworkTableInstance final {
*/
StringArrayTopic GetStringArrayTopic(std::string_view name) const;
/**
* Gets a protobuf serialized value topic.
*
* @param name topic name
* @return Topic
*/
template <wpi::ProtobufSerializable T>
ProtobufTopic<T> GetProtobufTopic(std::string_view name) const;
/**
* Gets a raw struct serialized value topic.
*
* @param name topic name
* @return Topic
*/
template <wpi::StructSerializable T>
StructTopic<T> GetStructTopic(std::string_view name) const;
/**
* Gets a raw struct serialized array topic.
*
* @param name topic name
* @return Topic
*/
template <wpi::StructSerializable T>
StructArrayTopic<T> GetStructArrayTopic(std::string_view name) const;
/**
* Get Published Topics.
*
@@ -718,6 +754,75 @@ class NetworkTableInstance final {
/** @} */
/**
* @{
* @name Schema Functions
*/
/**
* Returns whether there is a data schema already registered with the given
* name. This does NOT perform a check as to whether the schema has already
* been published by another node on the network.
*
* @param name Name (the string passed as the data type for topics using this
* schema)
* @return True if schema already registered
*/
bool HasSchema(std::string_view name) const;
/**
* Registers a data schema. Data schemas provide information for how a
* certain data type string can be decoded. The type string of a data schema
* indicates the type of the schema itself (e.g. "protobuf" for protobuf
* schemas, "struct" for struct schemas, etc). In NetworkTables, schemas are
* published just like normal topics, with the name being generated from the
* provided name: "/.schema/<name>". Duplicate calls to this function with
* the same name are silently ignored.
*
* @param name Name (the string passed as the data type for topics using this
* schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
void AddSchema(std::string_view name, std::string_view type,
std::span<const uint8_t> schema);
/**
* Registers a data schema. Data schemas provide information for how a
* certain data type string can be decoded. The type string of a data schema
* indicates the type of the schema itself (e.g. "protobuf" for protobuf
* schemas, "struct" for struct schemas, etc). In NetworkTables, schemas are
* published just like normal topics, with the name being generated from the
* provided name: "/.schema/<name>". Duplicate calls to this function with
* the same name are silently ignored.
*
* @param name Name (the string passed as the data type for topics using this
* schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
void AddSchema(std::string_view name, std::string_view type,
std::string_view schema);
/**
* Registers a protobuf schema. Duplicate calls to this function with the same
* name are silently ignored.
*
* @tparam T protobuf serializable type
* @param msg protobuf message
*/
template <wpi::ProtobufSerializable T>
void AddProtobufSchema(wpi::ProtobufMessage<T>& msg);
/**
* Registers a struct schema. Duplicate calls to this function with the same
* name are silently ignored.
*
* @param T struct serializable type
*/
template <wpi::StructSerializable T>
void AddStructSchema();
/**
* Equality operator. Returns true if both instances refer to the same
* native handle.

View File

@@ -38,6 +38,24 @@ inline NT_Inst NetworkTableInstance::GetHandle() const {
return m_handle;
}
template <wpi::ProtobufSerializable T>
inline ProtobufTopic<T> NetworkTableInstance::GetProtobufTopic(
std::string_view name) const {
return ProtobufTopic<T>{GetTopic(name)};
}
template <wpi::StructSerializable T>
inline StructTopic<T> NetworkTableInstance::GetStructTopic(
std::string_view name) const {
return StructTopic<T>{GetTopic(name)};
}
template <wpi::StructSerializable T>
inline StructArrayTopic<T> NetworkTableInstance::GetStructArrayTopic(
std::string_view name) const {
return StructArrayTopic<T>{GetTopic(name)};
}
inline std::vector<Topic> NetworkTableInstance::GetTopics() {
auto handles = ::nt::GetTopics(m_handle, "", 0);
return {handles.begin(), handles.end()};
@@ -223,4 +241,36 @@ inline NT_Listener NetworkTableInstance::AddLogger(unsigned int min_level,
return ::nt::AddLogger(m_handle, min_level, max_level, std::move(func));
}
inline bool NetworkTableInstance::HasSchema(std::string_view name) const {
return ::nt::HasSchema(m_handle, name);
}
inline void NetworkTableInstance::AddSchema(std::string_view name,
std::string_view type,
std::span<const uint8_t> schema) {
::nt::AddSchema(m_handle, name, type, schema);
}
inline void NetworkTableInstance::AddSchema(std::string_view name,
std::string_view type,
std::string_view schema) {
::nt::AddSchema(m_handle, name, type, schema);
}
template <wpi::ProtobufSerializable T>
void NetworkTableInstance::AddProtobufSchema(wpi::ProtobufMessage<T>& msg) {
msg.ForEachProtobufDescriptor(
[this](auto typeString) { return HasSchema(typeString); },
[this](auto typeString, auto schema) {
AddSchema(typeString, "proto:FileDescriptorProto", schema);
});
}
template <wpi::StructSerializable T>
void NetworkTableInstance::AddStructSchema() {
wpi::ForEachStructSchema<T>([this](auto typeString, auto schema) {
AddSchema(typeString, "structschema", schema);
});
}
} // namespace nt

View File

@@ -0,0 +1,474 @@
// 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.
#pragma once
#include <stdint.h>
#include <atomic>
#include <concepts>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
#include <wpi/SmallVector.h>
#include <wpi/mutex.h>
#include <wpi/protobuf/Protobuf.h>
#include "networktables/NetworkTableInstance.h"
#include "networktables/Topic.h"
#include "ntcore_cpp.h"
namespace wpi {
class json;
} // namespace wpi
namespace nt {
template <wpi::ProtobufSerializable T>
class ProtobufTopic;
/**
* NetworkTables protobuf-encoded value subscriber.
*/
template <wpi::ProtobufSerializable T>
class ProtobufSubscriber : public Subscriber {
public:
using TopicType = ProtobufTopic<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
ProtobufSubscriber() = default;
/**
* Construct from a subscriber handle; recommended to use
* ProtobufTopic::Subscribe() instead.
*
* @param handle Native handle
* @param msg Protobuf message
* @param defaultValue Default value
*/
ProtobufSubscriber(NT_Subscriber handle, wpi::ProtobufMessage<T> msg,
T defaultValue)
: Subscriber{handle},
m_msg{std::move(msg)},
m_defaultValue{std::move(defaultValue)} {}
ProtobufSubscriber(const ProtobufSubscriber&) = delete;
ProtobufSubscriber& operator=(const ProtobufSubscriber&) = delete;
ProtobufSubscriber(ProtobufSubscriber&& rhs)
: Subscriber{std::move(rhs)},
m_msg{std::move(rhs.m_msg)},
m_defaultValue{std::move(rhs.defaultValue)} {}
ProtobufSubscriber& operator=(ProtobufSubscriber&& rhs) {
Subscriber::operator=(std::move(rhs));
m_msg = std::move(rhs.m_msg);
m_defaultValue = std::move(rhs.defaultValue);
return *this;
}
/**
* Get the last published value.
* If no value has been published or the value cannot be unpacked, returns the
* stored default value.
*
* @return value
*/
ValueType Get() const { return Get(m_defaultValue); }
/**
* Get the last published value.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue.
*
* @param defaultValue default value to return if no value has been published
* @return value
*/
ValueType Get(const T& defaultValue) const {
return GetAtomic(defaultValue).value;
}
/**
* Get the last published value, replacing the contents in place of an
* existing object. If no value has been published or the value cannot be
* unpacked, does not replace the contents and returns false.
*
* @param[out] out object to replace contents of
* @return true if successful
*/
bool GetInto(T* out) {
wpi::SmallVector<uint8_t, 128> buf;
TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
if (view.value.empty()) {
return false;
} else {
std::scoped_lock lock{m_mutex};
return m_msg.UnpackInto(out, view.value);
}
}
/**
* Get the last published value along with its timestamp
* If no value has been published or the value cannot be unpacked, returns the
* stored default value and a timestamp of 0.
*
* @return timestamped value
*/
TimestampedValueType GetAtomic() const { return GetAtomic(m_defaultValue); }
/**
* Get the last published value along with its timestamp.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue and a timestamp of 0.
*
* @param defaultValue default value to return if no value has been published
* @return timestamped value
*/
TimestampedValueType GetAtomic(const T& defaultValue) const {
wpi::SmallVector<uint8_t, 128> buf;
TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
if (!view.value.empty()) {
std::scoped_lock lock{m_mutex};
if (auto optval = m_msg.Unpack(view.value)) {
return {view.time, view.serverTime, *optval};
}
}
return {0, 0, defaultValue};
}
/**
* Get an array of all valid value changes since the last call to ReadQueue.
* Also provides a timestamp for each value. Values that cannot be unpacked
* are dropped.
*
* @note The "poll storage" subscribe option can be used to set the queue
* depth.
*
* @return Array of timestamped values; empty array if no valid new changes
* have been published since the previous call.
*/
std::vector<TimestampedValueType> ReadQueue() {
auto raw = ::nt::ReadQueueRaw(m_subHandle);
std::vector<TimestampedValueType> rv;
rv.reserve(raw.size());
std::scoped_lock lock{m_mutex};
for (auto&& r : raw) {
if (auto optval = m_msg.Unpack(r.value)) {
rv.emplace_back(r.time, r.serverTime, *optval);
}
}
return rv;
}
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return ProtobufTopic<T>{::nt::GetTopicFromHandle(m_subHandle)};
}
private:
wpi::mutex m_mutex;
wpi::ProtobufMessage<T> m_msg;
ValueType m_defaultValue;
};
/**
* NetworkTables protobuf-encoded value publisher.
*/
template <wpi::ProtobufSerializable T>
class ProtobufPublisher : public Publisher {
public:
using TopicType = ProtobufTopic<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
ProtobufPublisher() = default;
/**
* Construct from a publisher handle; recommended to use
* ProtobufTopic::Publish() instead.
*
* @param handle Native handle
* @param msg Protobuf message
*/
explicit ProtobufPublisher(NT_Publisher handle, wpi::ProtobufMessage<T> msg)
: Publisher{handle}, m_msg{std::move(msg)} {}
ProtobufPublisher(const ProtobufPublisher&) = delete;
ProtobufPublisher& operator=(const ProtobufPublisher&) = delete;
ProtobufPublisher(ProtobufPublisher&& rhs)
: Publisher{std::move(rhs)},
m_msg{std::move(rhs.m_msg)},
m_schemaPublished{rhs.m_schemaPublished} {}
ProtobufPublisher& operator=(ProtobufPublisher&& rhs) {
Publisher::operator=(std::move(rhs));
m_msg = std::move(rhs.m_msg);
m_schemaPublished.clear();
if (rhs.m_schemaPublished.test()) {
m_schemaPublished.test_and_set();
}
return *this;
}
/**
* Publish a new value.
*
* @param value value to publish
* @param time timestamp; 0 indicates current NT time should be used
*/
void Set(const T& value, int64_t time = 0) {
wpi::SmallVector<uint8_t, 128> buf;
{
std::scoped_lock lock{m_mutex};
if (!m_schemaPublished.test_and_set()) {
GetTopic().GetInstance().template AddProtobufSchema<T>(m_msg);
}
m_msg.Pack(buf, value);
}
::nt::SetRaw(m_pubHandle, buf, time);
}
/**
* Publish a default value.
* On reconnect, a default value will never be used in preference to a
* published value.
*
* @param value value
*/
void SetDefault(const T& value) {
wpi::SmallVector<uint8_t, 128> buf;
{
std::scoped_lock lock{m_mutex};
if (!m_schemaPublished.test_and_set()) {
GetTopic().GetInstance().template AddProtobufSchema<T>(m_msg);
}
m_msg.Pack(buf, value);
}
::nt::SetDefaultRaw(m_pubHandle, buf);
}
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return ProtobufTopic<T>{::nt::GetTopicFromHandle(m_pubHandle)};
}
private:
wpi::mutex m_mutex;
wpi::ProtobufMessage<T> m_msg;
std::atomic_flag m_schemaPublished = ATOMIC_FLAG_INIT;
};
/**
* NetworkTables protobuf-encoded value entry.
*
* @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
*/
template <wpi::ProtobufSerializable T>
class ProtobufEntry final : public ProtobufSubscriber<T>,
public ProtobufPublisher<T> {
public:
using SubscriberType = ProtobufSubscriber<T>;
using PublisherType = ProtobufPublisher<T>;
using TopicType = ProtobufTopic<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
ProtobufEntry() = default;
/**
* Construct from an entry handle; recommended to use
* ProtobufTopic::GetEntry() instead.
*
* @param handle Native handle
* @param msg Protobuf message
* @param defaultValue Default value
*/
ProtobufEntry(NT_Entry handle, wpi::ProtobufMessage<T> msg, T defaultValue)
: ProtobufSubscriber<T>{handle, std::move(msg), std::move(defaultValue)},
ProtobufPublisher<T>{handle, {}} {}
/**
* Determines if the native handle is valid.
*
* @return True if the native handle is valid, false otherwise.
*/
explicit operator bool() const { return this->m_subHandle != 0; }
/**
* Gets the native handle for the entry.
*
* @return Native handle
*/
NT_Entry GetHandle() const { return this->m_subHandle; }
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return ProtobufTopic<T>{::nt::GetTopicFromHandle(this->m_subHandle)};
}
/**
* Stops publishing the entry if it's published.
*/
void Unpublish() { ::nt::Unpublish(this->m_pubHandle); }
};
/**
* NetworkTables protobuf-encoded value topic.
*/
template <wpi::ProtobufSerializable T>
class ProtobufTopic final : public Topic {
public:
using SubscriberType = ProtobufSubscriber<T>;
using PublisherType = ProtobufPublisher<T>;
using EntryType = ProtobufEntry<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
ProtobufTopic() = default;
/**
* Construct from a topic handle; recommended to use
* NetworkTableInstance::GetProtobufTopic() instead.
*
* @param handle Native handle
*/
explicit ProtobufTopic(NT_Topic handle) : Topic{handle} {}
/**
* Construct from a generic topic.
*
* @param topic Topic
*/
explicit ProtobufTopic(Topic topic) : Topic{topic} {}
/**
* Create a new subscriber to the topic.
*
* <p>The subscriber is only active as long as the returned object
* is not destroyed.
*
* @note Subscribers that do not match the published data type do not return
* any values. To determine if the data type matches, use the appropriate
* Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options subscribe options
* @return subscriber
*/
[[nodiscard]]
SubscriberType Subscribe(
T defaultValue, const PubSubOptions& options = kDefaultPubSubOptions) {
wpi::ProtobufMessage<T> msg;
auto typeString = msg.GetTypeString();
return ProtobufSubscriber<T>{
::nt::Subscribe(m_handle, NT_RAW, typeString, options), std::move(msg),
std::move(defaultValue)};
}
/**
* Create a new publisher to the topic.
*
* The publisher is only active as long as the returned object
* is not destroyed.
*
* @note It is not possible to publish two different data types to the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored). To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param options publish options
* @return publisher
*/
[[nodiscard]]
PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions) {
wpi::ProtobufMessage<T> msg;
auto typeString = msg.GetTypeString();
return ProtobufPublisher<T>{
::nt::Publish(m_handle, NT_RAW, typeString, options), std::move(msg)};
}
/**
* Create a new publisher to the topic, with type string and initial
* properties.
*
* The publisher is only active as long as the returned object
* is not destroyed.
*
* @note It is not possible to publish two different data types to the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored). To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param properties JSON properties
* @param options publish options
* @return publisher
*/
[[nodiscard]]
PublisherType PublishEx(
const wpi::json& properties,
const PubSubOptions& options = kDefaultPubSubOptions) {
wpi::ProtobufMessage<T> msg;
auto typeString = msg.GetTypeString();
return ProtobufPublisher<T>{
::nt::PublishEx(m_handle, NT_RAW, typeString, properties, options),
std::move(msg)};
}
/**
* Create a new entry for the topic.
*
* Entries act as a combination of a subscriber and a weak publisher. The
* subscriber is active as long as the entry is not destroyed. The publisher
* is created when the entry is first written to, and remains active until
* either Unpublish() is called or the entry is destroyed.
*
* @note It is not possible to use two different data types with the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored), and the entry
* will show no new values if the data type does not match. To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options publish and/or subscribe options
* @return entry
*/
[[nodiscard]]
EntryType GetEntry(T defaultValue,
const PubSubOptions& options = kDefaultPubSubOptions) {
wpi::ProtobufMessage<T> msg;
auto typeString = msg.GetTypeString();
return ProtobufEntry<T>{
::nt::GetEntry(m_handle, NT_RAW, typeString, options), std::move(msg),
std::move(defaultValue)};
}
};
} // namespace nt

View File

@@ -0,0 +1,593 @@
// 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.
#pragma once
#include <stdint.h>
#include <atomic>
#include <ranges>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
#include <wpi/SmallVector.h>
#include <wpi/mutex.h>
#include <wpi/struct/Struct.h>
#include "networktables/NetworkTableInstance.h"
#include "networktables/Topic.h"
#include "ntcore_cpp.h"
namespace wpi {
class json;
} // namespace wpi
namespace nt {
template <wpi::StructSerializable T>
class StructArrayTopic;
/**
* NetworkTables struct-encoded value array subscriber.
*/
template <wpi::StructSerializable T>
class StructArraySubscriber : public Subscriber {
using S = wpi::Struct<T>;
public:
using TopicType = StructArrayTopic<T>;
using ValueType = std::vector<T>;
using ParamType = std::span<const T>;
using TimestampedValueType = Timestamped<ValueType>;
StructArraySubscriber() = default;
/**
* Construct from a subscriber handle; recommended to use
* StructTopic::Subscribe() instead.
*
* @param handle Native handle
* @param defaultValue Default value
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
StructArraySubscriber(NT_Subscriber handle, U&& defaultValue)
: Subscriber{handle},
m_defaultValue{defaultValue.begin(), defaultValue.end()} {
}
/**
* Get the last published value.
* If no value has been published or the value cannot be unpacked, returns the
* stored default value.
*
* @return value
*/
ValueType Get() const { return Get(m_defaultValue); }
/**
* Get the last published value.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue.
*
* @param defaultValue default value to return if no value has been published
* @return value
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
ValueType Get(U&& defaultValue) const {
return GetAtomic(std::forward<U>(defaultValue)).value;
}
/**
* Get the last published value.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue.
*
* @param defaultValue default value to return if no value has been published
* @return value
*/
ValueType Get(std::span<const T> defaultValue) const {
return GetAtomic(defaultValue).value;
}
/**
* Get the last published value along with its timestamp
* If no value has been published or the value cannot be unpacked, returns the
* stored default value and a timestamp of 0.
*
* @return timestamped value
*/
TimestampedValueType GetAtomic() const { return GetAtomic(m_defaultValue); }
/**
* Get the last published value along with its timestamp.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue and a timestamp of 0.
*
* @param defaultValue default value to return if no value has been published
* @return timestamped value
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
TimestampedValueType GetAtomic(U&& defaultValue) const {
wpi::SmallVector<uint8_t, 128> buf;
TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
if (view.value.size() == 0 || (view.value.size() % S::kSize) != 0) {
return {0, 0, std::forward<U>(defaultValue)};
}
TimestampedValueType rv{view.time, view.serverTime, {}};
rv.value.reserve(view.value.size() / S::kSize);
for (auto in = view.value.begin(), end = view.value.end(); in != end;
in += S::kSize) {
rv.value.emplace_back(
S::Unpack(std::span<const uint8_t, S::kSize>{in, in + S::kSize}));
}
return rv;
}
/**
* Get the last published value along with its timestamp.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue and a timestamp of 0.
*
* @param defaultValue default value to return if no value has been published
* @return timestamped value
*/
TimestampedValueType GetAtomic(std::span<const T> defaultValue) const {
wpi::SmallVector<uint8_t, 128> buf;
TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
if (view.value.size() == 0 || (view.value.size() % S::kSize) != 0) {
return {0, 0, {defaultValue.begin(), defaultValue.end()}};
}
TimestampedValueType rv{view.time, view.serverTime, {}};
rv.value.reserve(view.value.size() / S::kSize);
for (auto in = view.value.begin(), end = view.value.end(); in != end;
in += S::kSize) {
rv.value.emplace_back(
S::Unpack(std::span<const uint8_t, S::kSize>{in, in + S::kSize}));
}
return rv;
}
/**
* Get an array of all valid value changes since the last call to ReadQueue.
* Also provides a timestamp for each value. Values that cannot be unpacked
* are dropped.
*
* @note The "poll storage" subscribe option can be used to set the queue
* depth.
*
* @return Array of timestamped values; empty array if no valid new changes
* have been published since the previous call.
*/
std::vector<TimestampedValueType> ReadQueue() {
auto raw = ::nt::ReadQueueRaw(m_subHandle);
std::vector<TimestampedValueType> rv;
rv.reserve(raw.size());
for (auto&& r : raw) {
if (r.value.size() == 0 || (r.value.size() % S::kSize) != 0) {
continue;
}
std::vector<T> values;
values.reserve(r.value.size() / S::kSize);
for (auto in = r.value.begin(), end = r.value.end(); in != end;
in += S::kSize) {
values.emplace_back(
S::Unpack(std::span<const uint8_t, S::kSize>{in, in + S::kSize}));
}
rv.emplace_back(r.time, r.serverTime, std::move(values));
}
return rv;
}
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return StructArrayTopic<T>{::nt::GetTopicFromHandle(m_subHandle)};
}
private:
ValueType m_defaultValue;
};
/**
* NetworkTables struct-encoded value array publisher.
*/
template <wpi::StructSerializable T>
class StructArrayPublisher : public Publisher {
using S = wpi::Struct<T>;
public:
using TopicType = StructArrayTopic<T>;
using ValueType = std::vector<T>;
using ParamType = std::span<const T>;
using TimestampedValueType = Timestamped<ValueType>;
StructArrayPublisher() = default;
/**
* Construct from a publisher handle; recommended to use
* StructTopic::Publish() instead.
*
* @param handle Native handle
*/
explicit StructArrayPublisher(NT_Publisher handle) : Publisher{handle} {}
StructArrayPublisher(const StructArrayPublisher&) = delete;
StructArrayPublisher& operator=(const StructArrayPublisher&) = delete;
StructArrayPublisher(StructArrayPublisher&& rhs)
: Publisher{std::move(rhs)},
m_buf{std::move(rhs.m_buf)},
m_schemaPublished{rhs.m_schemaPublished} {}
StructArrayPublisher& operator=(StructArrayPublisher&& rhs) {
Publisher::operator=(std::move(rhs));
m_buf = std::move(rhs.m_buf);
m_schemaPublished.clear();
if (rhs.m_schemaPublished.test()) {
m_schemaPublished.test_and_set();
}
return *this;
}
/**
* Publish a new value.
*
* @param value value to publish
* @param time timestamp; 0 indicates current NT time should be used
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
void Set(U&& value, int64_t time = 0) {
if (!m_schemaPublished.test_and_set()) {
GetTopic().GetInstance().template AddStructSchema<T>();
}
m_buf.Write(std::forward<U>(value),
[&](auto bytes) { ::nt::SetRaw(m_pubHandle, bytes, time); });
}
/**
* Publish a new value.
*
* @param value value to publish
* @param time timestamp; 0 indicates current NT time should be used
*/
void Set(std::span<const T> value, int64_t time = 0) {
m_buf.Write(value,
[&](auto bytes) { ::nt::SetRaw(m_pubHandle, bytes, time); });
}
/**
* Publish a default value.
* On reconnect, a default value will never be used in preference to a
* published value.
*
* @param value value
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
void SetDefault(U&& value) {
if (!m_schemaPublished.test_and_set()) {
GetTopic().GetInstance().template AddStructSchema<T>();
}
m_buf.Write(std::forward<U>(value),
[&](auto bytes) { ::nt::SetDefaultRaw(m_pubHandle, bytes); });
}
/**
* Publish a default value.
* On reconnect, a default value will never be used in preference to a
* published value.
*
* @param value value
*/
void SetDefault(std::span<const T> value) {
m_buf.Write(value,
[&](auto bytes) { ::nt::SetDefaultRaw(m_pubHandle, bytes); });
}
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return StructArrayTopic<T>{::nt::GetTopicFromHandle(m_pubHandle)};
}
private:
wpi::StructArrayBuffer<T> m_buf;
std::atomic_flag m_schemaPublished = ATOMIC_FLAG_INIT;
};
/**
* NetworkTables struct-encoded value array entry.
*
* @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
*/
template <wpi::StructSerializable T>
class StructArrayEntry final : public StructArraySubscriber<T>,
public StructArrayPublisher<T> {
public:
using SubscriberType = StructArraySubscriber<T>;
using PublisherType = StructArrayPublisher<T>;
using TopicType = StructArrayTopic<T>;
using ValueType = std::vector<T>;
using ParamType = std::span<const T>;
using TimestampedValueType = Timestamped<ValueType>;
StructArrayEntry() = default;
/**
* Construct from an entry handle; recommended to use
* StructTopic::GetEntry() instead.
*
* @param handle Native handle
* @param defaultValue Default value
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
StructArrayEntry(NT_Entry handle, U&& defaultValue)
: StructArraySubscriber<T>{handle, defaultValue},
StructArrayPublisher<T>{handle} {
}
/**
* Determines if the native handle is valid.
*
* @return True if the native handle is valid, false otherwise.
*/
explicit operator bool() const { return this->m_subHandle != 0; }
/**
* Gets the native handle for the entry.
*
* @return Native handle
*/
NT_Entry GetHandle() const { return this->m_subHandle; }
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return StructArrayTopic<T>{::nt::GetTopicFromHandle(this->m_subHandle)};
}
/**
* Stops publishing the entry if it's published.
*/
void Unpublish() { ::nt::Unpublish(this->m_pubHandle); }
};
/**
* NetworkTables struct-encoded value array topic.
*/
template <wpi::StructSerializable T>
class StructArrayTopic final : public Topic {
public:
using SubscriberType = StructArraySubscriber<T>;
using PublisherType = StructArrayPublisher<T>;
using EntryType = StructArrayEntry<T>;
using ValueType = std::vector<T>;
using ParamType = std::span<const T>;
using TimestampedValueType = Timestamped<ValueType>;
StructArrayTopic() = default;
/**
* Construct from a topic handle; recommended to use
* NetworkTableInstance::GetStructTopic() instead.
*
* @param handle Native handle
*/
explicit StructArrayTopic(NT_Topic handle) : Topic{handle} {}
/**
* Construct from a generic topic.
*
* @param topic Topic
*/
explicit StructArrayTopic(Topic topic) : Topic{topic} {}
/**
* Create a new subscriber to the topic.
*
* <p>The subscriber is only active as long as the returned object
* is not destroyed.
*
* @note Subscribers that do not match the published data type do not return
* any values. To determine if the data type matches, use the appropriate
* Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options subscribe options
* @return subscriber
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
[[nodiscard]]
SubscriberType Subscribe(
U&& defaultValue, const PubSubOptions& options = kDefaultPubSubOptions) {
return StructArraySubscriber<T>{
::nt::Subscribe(
m_handle, NT_RAW,
wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), options),
defaultValue};
}
/**
* Create a new subscriber to the topic.
*
* <p>The subscriber is only active as long as the returned object
* is not destroyed.
*
* @note Subscribers that do not match the published data type do not return
* any values. To determine if the data type matches, use the appropriate
* Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options subscribe options
* @return subscriber
*/
[[nodiscard]]
SubscriberType Subscribe(
std::span<const T> defaultValue,
const PubSubOptions& options = kDefaultPubSubOptions) {
return StructArraySubscriber<T>{
::nt::Subscribe(
m_handle, NT_RAW,
wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), options),
defaultValue};
}
/**
* Create a new publisher to the topic.
*
* The publisher is only active as long as the returned object
* is not destroyed.
*
* @note It is not possible to publish two different data types to the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored). To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param options publish options
* @return publisher
*/
[[nodiscard]]
PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions) {
return StructArrayPublisher<T>{::nt::Publish(
m_handle, NT_RAW,
wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), options)};
}
/**
* Create a new publisher to the topic, with type string and initial
* properties.
*
* The publisher is only active as long as the returned object
* is not destroyed.
*
* @note It is not possible to publish two different data types to the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored). To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param properties JSON properties
* @param options publish options
* @return publisher
*/
[[nodiscard]]
PublisherType PublishEx(
const wpi::json& properties,
const PubSubOptions& options = kDefaultPubSubOptions) {
return StructArrayPublisher<T>{::nt::PublishEx(
m_handle, NT_RAW,
wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(), properties,
options)};
}
/**
* Create a new entry for the topic.
*
* Entries act as a combination of a subscriber and a weak publisher. The
* subscriber is active as long as the entry is not destroyed. The publisher
* is created when the entry is first written to, and remains active until
* either Unpublish() is called or the entry is destroyed.
*
* @note It is not possible to use two different data types with the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored), and the entry
* will show no new values if the data type does not match. To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options publish and/or subscribe options
* @return entry
*/
template <typename U>
#if __cpp_lib_ranges >= 201911L
requires std::ranges::range<U> &&
std::convertible_to<std::ranges::range_value_t<U>, T>
#endif
[[nodiscard]]
EntryType GetEntry(U&& defaultValue,
const PubSubOptions& options = kDefaultPubSubOptions) {
return StructArrayEntry<T>{
::nt::GetEntry(m_handle, NT_RAW,
wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(),
options),
defaultValue};
}
/**
* Create a new entry for the topic.
*
* Entries act as a combination of a subscriber and a weak publisher. The
* subscriber is active as long as the entry is not destroyed. The publisher
* is created when the entry is first written to, and remains active until
* either Unpublish() is called or the entry is destroyed.
*
* @note It is not possible to use two different data types with the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored), and the entry
* will show no new values if the data type does not match. To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options publish and/or subscribe options
* @return entry
*/
[[nodiscard]]
EntryType GetEntry(std::span<const T> defaultValue,
const PubSubOptions& options = kDefaultPubSubOptions) {
return StructArrayEntry<T>{
::nt::GetEntry(m_handle, NT_RAW,
wpi::MakeStructArrayTypeString<T, std::dynamic_extent>(),
options),
defaultValue};
}
};
} // namespace nt

View File

@@ -0,0 +1,438 @@
// 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.
#pragma once
#include <stdint.h>
#include <atomic>
#include <concepts>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
#include <wpi/SmallVector.h>
#include <wpi/struct/Struct.h>
#include "networktables/NetworkTableInstance.h"
#include "networktables/Topic.h"
#include "ntcore_cpp.h"
namespace wpi {
class json;
} // namespace wpi
namespace nt {
template <wpi::StructSerializable T>
class StructTopic;
/**
* NetworkTables struct-encoded value subscriber.
*/
template <wpi::StructSerializable T>
class StructSubscriber : public Subscriber {
using S = wpi::Struct<T>;
public:
using TopicType = StructTopic<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
StructSubscriber() = default;
/**
* Construct from a subscriber handle; recommended to use
* StructTopic::Subscribe() instead.
*
* @param handle Native handle
* @param defaultValue Default value
*/
StructSubscriber(NT_Subscriber handle, T defaultValue)
: Subscriber{handle}, m_defaultValue{std::move(defaultValue)} {}
/**
* Get the last published value.
* If no value has been published or the value cannot be unpacked, returns the
* stored default value.
*
* @return value
*/
ValueType Get() const { return Get(m_defaultValue); }
/**
* Get the last published value.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue.
*
* @param defaultValue default value to return if no value has been published
* @return value
*/
ValueType Get(const T& defaultValue) const {
return GetAtomic(defaultValue).value;
}
/**
* Get the last published value, replacing the contents in place of an
* existing object. If no value has been published or the value cannot be
* unpacked, does not replace the contents and returns false.
*
* @param[out] out object to replace contents of
* @return true if successful
*/
bool GetInto(T* out) {
wpi::SmallVector<uint8_t, S::kSize> buf;
TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
if (view.value.size() < S::kSize) {
return false;
} else {
wpi::UnpackStructInto(out, view.value.subspan<0, S::kSize>());
return true;
}
}
/**
* Get the last published value along with its timestamp
* If no value has been published or the value cannot be unpacked, returns the
* stored default value and a timestamp of 0.
*
* @return timestamped value
*/
TimestampedValueType GetAtomic() const { return GetAtomic(m_defaultValue); }
/**
* Get the last published value along with its timestamp.
* If no value has been published or the value cannot be unpacked, returns the
* passed defaultValue and a timestamp of 0.
*
* @param defaultValue default value to return if no value has been published
* @return timestamped value
*/
TimestampedValueType GetAtomic(const T& defaultValue) const {
wpi::SmallVector<uint8_t, S::kSize> buf;
TimestampedRawView view = ::nt::GetAtomicRaw(m_subHandle, buf, {});
if (view.value.size() < S::kSize) {
return {0, 0, defaultValue};
} else {
return {view.time, view.serverTime,
S::Unpack(view.value.subspan<0, S::kSize>())};
}
}
/**
* Get an array of all valid value changes since the last call to ReadQueue.
* Also provides a timestamp for each value. Values that cannot be unpacked
* are dropped.
*
* @note The "poll storage" subscribe option can be used to set the queue
* depth.
*
* @return Array of timestamped values; empty array if no valid new changes
* have been published since the previous call.
*/
std::vector<TimestampedValueType> ReadQueue() {
auto raw = ::nt::ReadQueueRaw(m_subHandle);
std::vector<TimestampedValueType> rv;
rv.reserve(raw.size());
for (auto&& r : raw) {
if (r.value.size() < S::kSize) {
continue;
} else {
rv.emplace_back(
r.time, r.serverTime,
S::Unpack(
std::span<const uint8_t>(r.value).subspan<0, S::kSize>()));
}
}
return rv;
}
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return StructTopic<T>{::nt::GetTopicFromHandle(m_subHandle)};
}
private:
ValueType m_defaultValue;
};
/**
* NetworkTables struct-encoded value publisher.
*/
template <wpi::StructSerializable T>
class StructPublisher : public Publisher {
using S = wpi::Struct<T>;
public:
using TopicType = StructTopic<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
StructPublisher() = default;
StructPublisher(const StructPublisher&) = delete;
StructPublisher& operator=(const StructPublisher&) = delete;
StructPublisher(StructPublisher&& rhs)
: Publisher{std::move(rhs)}, m_schemaPublished{rhs.m_schemaPublished} {}
StructPublisher& operator=(StructPublisher&& rhs) {
Publisher::operator=(std::move(rhs));
m_schemaPublished.clear();
if (rhs.m_schemaPublished.test()) {
m_schemaPublished.test_and_set();
}
return *this;
}
/**
* Construct from a publisher handle; recommended to use
* StructTopic::Publish() instead.
*
* @param handle Native handle
*/
explicit StructPublisher(NT_Publisher handle) : Publisher{handle} {}
/**
* Publish a new value.
*
* @param value value to publish
* @param time timestamp; 0 indicates current NT time should be used
*/
void Set(const T& value, int64_t time = 0) {
if (!m_schemaPublished.test_and_set()) {
GetTopic().GetInstance().template AddStructSchema<T>();
}
uint8_t buf[S::kSize];
S::Pack(buf, value);
::nt::SetRaw(m_pubHandle, buf, time);
}
/**
* Publish a default value.
* On reconnect, a default value will never be used in preference to a
* published value.
*
* @param value value
*/
void SetDefault(const T& value) {
if (!m_schemaPublished.test_and_set()) {
GetTopic().GetInstance().template AddStructSchema<T>();
}
uint8_t buf[S::kSize];
S::Pack(buf, value);
::nt::SetDefaultRaw(m_pubHandle, buf);
}
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return StructTopic<T>{::nt::GetTopicFromHandle(m_pubHandle)};
}
private:
std::atomic_flag m_schemaPublished = ATOMIC_FLAG_INIT;
};
/**
* NetworkTables struct-encoded value entry.
*
* @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
*/
template <wpi::StructSerializable T>
class StructEntry final : public StructSubscriber<T>,
public StructPublisher<T> {
public:
using SubscriberType = StructSubscriber<T>;
using PublisherType = StructPublisher<T>;
using TopicType = StructTopic<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
StructEntry() = default;
/**
* Construct from an entry handle; recommended to use
* StructTopic::GetEntry() instead.
*
* @param handle Native handle
* @param defaultValue Default value
*/
StructEntry(NT_Entry handle, T defaultValue)
: StructSubscriber<T>{handle, std::move(defaultValue)},
StructPublisher<T>{handle} {}
/**
* Determines if the native handle is valid.
*
* @return True if the native handle is valid, false otherwise.
*/
explicit operator bool() const { return this->m_subHandle != 0; }
/**
* Gets the native handle for the entry.
*
* @return Native handle
*/
NT_Entry GetHandle() const { return this->m_subHandle; }
/**
* Get the corresponding topic.
*
* @return Topic
*/
TopicType GetTopic() const {
return StructTopic<T>{::nt::GetTopicFromHandle(this->m_subHandle)};
}
/**
* Stops publishing the entry if it's published.
*/
void Unpublish() { ::nt::Unpublish(this->m_pubHandle); }
};
/**
* NetworkTables struct-encoded value topic.
*/
template <wpi::StructSerializable T>
class StructTopic final : public Topic {
public:
using SubscriberType = StructSubscriber<T>;
using PublisherType = StructPublisher<T>;
using EntryType = StructEntry<T>;
using ValueType = T;
using ParamType = const T&;
using TimestampedValueType = Timestamped<T>;
StructTopic() = default;
/**
* Construct from a topic handle; recommended to use
* NetworkTableInstance::GetStructTopic() instead.
*
* @param handle Native handle
*/
explicit StructTopic(NT_Topic handle) : Topic{handle} {}
/**
* Construct from a generic topic.
*
* @param topic Topic
*/
explicit StructTopic(Topic topic) : Topic{topic} {}
/**
* Create a new subscriber to the topic.
*
* <p>The subscriber is only active as long as the returned object
* is not destroyed.
*
* @note Subscribers that do not match the published data type do not return
* any values. To determine if the data type matches, use the appropriate
* Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options subscribe options
* @return subscriber
*/
[[nodiscard]]
SubscriberType Subscribe(
T defaultValue, const PubSubOptions& options = kDefaultPubSubOptions) {
return StructSubscriber<T>{
::nt::Subscribe(m_handle, NT_RAW, wpi::GetStructTypeString<T>(),
options),
std::move(defaultValue)};
}
/**
* Create a new publisher to the topic.
*
* The publisher is only active as long as the returned object
* is not destroyed.
*
* @note It is not possible to publish two different data types to the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored). To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param options publish options
* @return publisher
*/
[[nodiscard]]
PublisherType Publish(const PubSubOptions& options = kDefaultPubSubOptions) {
return StructPublisher<T>{::nt::Publish(
m_handle, NT_RAW, wpi::GetStructTypeString<T>(), options)};
}
/**
* Create a new publisher to the topic, with type string and initial
* properties.
*
* The publisher is only active as long as the returned object
* is not destroyed.
*
* @note It is not possible to publish two different data types to the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored). To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param properties JSON properties
* @param options publish options
* @return publisher
*/
[[nodiscard]]
PublisherType PublishEx(
const wpi::json& properties,
const PubSubOptions& options = kDefaultPubSubOptions) {
return StructPublisher<T>{::nt::PublishEx(
m_handle, NT_RAW, wpi::GetStructTypeString<T>(), properties, options)};
}
/**
* Create a new entry for the topic.
*
* Entries act as a combination of a subscriber and a weak publisher. The
* subscriber is active as long as the entry is not destroyed. The publisher
* is created when the entry is first written to, and remains active until
* either Unpublish() is called or the entry is destroyed.
*
* @note It is not possible to use two different data types with the same
* topic. Conflicts between publishers are typically resolved by the
* server on a first-come, first-served basis. Any published values that
* do not match the topic's data type are dropped (ignored), and the entry
* will show no new values if the data type does not match. To determine
* if the data type matches, use the appropriate Topic functions.
*
* @param defaultValue default value used when a default is not provided to a
* getter function
* @param options publish and/or subscribe options
* @return entry
*/
[[nodiscard]]
EntryType GetEntry(T defaultValue,
const PubSubOptions& options = kDefaultPubSubOptions) {
return StructEntry<T>{
::nt::GetEntry(m_handle, NT_RAW, wpi::GetStructTypeString<T>(),
options),
std::move(defaultValue)};
}
};
} // namespace nt

View File

@@ -6,7 +6,6 @@
#include <string>
#include "networktables/NetworkTableInstance.h"
#include "networktables/NetworkTableType.h"
#include "networktables/Topic.h"
#include "ntcore_c.h"
@@ -14,10 +13,6 @@
namespace nt {
inline NetworkTableInstance Topic::GetInstance() const {
return NetworkTableInstance{GetInstanceFromHandle(m_handle)};
}
inline std::string Topic::GetName() const {
return ::nt::GetTopicName(m_handle);
}

View File

@@ -1435,6 +1435,44 @@ NT_Listener NT_AddPolledLogger(NT_ListenerPoller poller, unsigned int min_level,
/** @} */
/**
* @defgroup ntcore_schema_cfunc Schema Functions
* @{
*/
/**
* Returns whether there is a data schema already registered with the given
* name. This does NOT perform a check as to whether the schema has already
* been published by another node on the network.
*
* @param inst instance
* @param name Name (the string passed as the data type for topics using this
* schema)
* @return True if schema already registered
*/
NT_Bool NT_HasSchema(NT_Inst inst, const char* name);
/**
* Registers a data schema. Data schemas provide information for how a
* certain data type string can be decoded. The type string of a data schema
* indicates the type of the schema itself (e.g. "protobuf" for protobuf
* schemas, "struct" for struct schemas, etc). In NetworkTables, schemas are
* published just like normal topics, with the name being generated from the
* provided name: "/.schema/<name>". Duplicate calls to this function with
* the same name are silently ignored.
*
* @param inst instance
* @param name Name (the string passed as the data type for topics using this
* schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
* @param schemaSize Size of schema data
*/
void NT_AddSchema(NT_Inst inst, const char* name, const char* type,
const uint8_t* schema, size_t schemaSize);
/** @} */
/**
* @defgroup ntcore_interop_cfunc Interop Utility Functions
* @{

View File

@@ -1300,6 +1300,66 @@ NT_Listener AddLogger(NT_Inst inst, unsigned int min_level,
NT_Listener AddPolledLogger(NT_ListenerPoller poller, unsigned int min_level,
unsigned int max_level);
/** @} */
/**
* @defgroup ntcore_schema_func Schema Functions
* @{
*/
/**
* Returns whether there is a data schema already registered with the given
* name. This does NOT perform a check as to whether the schema has already
* been published by another node on the network.
*
* @param inst instance
* @param name Name (the string passed as the data type for topics using this
* schema)
* @return True if schema already registered
*/
bool HasSchema(NT_Inst inst, std::string_view name);
/**
* Registers a data schema. Data schemas provide information for how a
* certain data type string can be decoded. The type string of a data schema
* indicates the type of the schema itself (e.g. "protobuf" for protobuf
* schemas, "struct" for struct schemas, etc). In NetworkTables, schemas are
* published just like normal topics, with the name being generated from the
* provided name: "/.schema/<name>". Duplicate calls to this function with
* the same name are silently ignored.
*
* @param inst instance
* @param name Name (the string passed as the data type for topics using this
* schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
void AddSchema(NT_Inst inst, std::string_view name, std::string_view type,
std::span<const uint8_t> schema);
/**
* Registers a data schema. Data schemas provide information for how a
* certain data type string can be decoded. The type string of a data schema
* indicates the type of the schema itself (e.g. "protobuf" for protobuf
* schemas, "struct" for struct schemas, etc). In NetworkTables, schemas are
* published just like normal topics, with the name being generated from the
* provided name: "/.schema/<name>". Duplicate calls to this function with
* the same name are silently ignored.
*
* @param inst instance
* @param name Name (the string passed as the data type for topics using this
* schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
inline void AddSchema(NT_Inst inst, std::string_view name,
std::string_view type, std::string_view schema) {
AddSchema(
inst, name, type,
std::span<const uint8_t>{reinterpret_cast<const uint8_t*>(schema.data()),
schema.size()});
}
/** @} */
/** @} */

View File

@@ -67,8 +67,8 @@ TEST_F(WireDecodeTextClientTest, ErrorEmpty) {
logger,
Call(_, _, _,
"could not decode JSON message: [json.exception.parse_error.101] "
"parse error at 1: syntax error - "
"unexpected end of input; expected '[', '{', or a literal"sv));
"parse error at line 1, column 1: syntax error while parsing value "
"- unexpected end of input; expected '[', '{', or a literal"sv));
net::WireDecodeText("", handler, logger);
}
@@ -77,8 +77,8 @@ TEST_F(WireDecodeTextClientTest, ErrorBadJson1) {
logger,
Call(_, _, _,
"could not decode JSON message: [json.exception.parse_error.101] "
"parse error at 2: syntax error - "
"unexpected end of input; expected '[', '{', or a literal"sv));
"parse error at line 1, column 2: syntax error while parsing value "
"- unexpected end of input; expected '[', '{', or a literal"sv));
net::WireDecodeText("[", handler, logger);
}
@@ -87,8 +87,8 @@ TEST_F(WireDecodeTextClientTest, ErrorBadJson2) {
logger,
Call(_, _, _,
"could not decode JSON message: [json.exception.parse_error.101] "
"parse error at 3: syntax error - "
"unexpected end of input; expected string literal"sv));
"parse error at line 1, column 3: syntax error while parsing object "
"key - unexpected end of input; expected string literal"sv));
net::WireDecodeText("[{", handler, logger);
}

View File

@@ -264,12 +264,18 @@ Thread::~Thread() {
void Thread::Main() {
// based on free disk space, scan for "old" FRC_*.wpilog files and remove
{
uintmax_t freeSpace = fs::space(m_logDir).free;
std::error_code ec;
uintmax_t freeSpace;
auto freeSpaceInfo = fs::space(m_logDir, ec);
if (!ec) {
freeSpace = freeSpaceInfo.available;
} else {
freeSpace = UINTMAX_MAX;
}
if (freeSpace < kFreeSpaceThreshold) {
// Delete oldest FRC_*.wpilog files (ignore FRC_TBD_*.wpilog as we just
// created one)
std::vector<fs::directory_entry> entries;
std::error_code ec;
for (auto&& entry : fs::directory_iterator{m_logDir, ec}) {
auto stem = entry.path().stem().string();
if (wpi::starts_with(stem, "FRC_") &&
@@ -462,6 +468,9 @@ static Instance& GetInstance(std::string_view dir = "",
std::string_view filename = "",
double period = 0.25) {
static Instance instance(dir, filename, period);
if (!instance.owner) {
instance.owner.Start(MakeLogDir(dir), filename, period);
}
return instance;
}
@@ -470,6 +479,12 @@ void DataLogManager::Start(std::string_view dir, std::string_view filename,
GetInstance(dir, filename, period);
}
void DataLogManager::Stop() {
auto& inst = GetInstance();
inst.owner.GetThread()->m_log.Stop();
inst.owner.Stop();
}
void DataLogManager::Log(std::string_view message) {
GetInstance().owner.GetThread()->m_messageLog.Append(message);
fmt::print("{}\n", message);
@@ -503,6 +518,10 @@ void DLM_Start(const char* dir, const char* filename, double period) {
DataLogManager::Start(dir, filename, period);
}
void DLM_Stop(void) {
DataLogManager::Stop();
}
void DLM_Log(const char* message) {
DataLogManager::Log(message);
}

View File

@@ -52,6 +52,11 @@ class DataLogManager final {
static void Start(std::string_view dir = "", std::string_view filename = "",
double period = 0.25);
/**
* Stop data log manager.
*/
static void Stop();
/**
* Log a message to the "messages" entry. The message is also printed to
* standard output (followed by a newline).
@@ -110,6 +115,11 @@ struct WPI_DataLog;
*/
void DLM_Start(const char* dir, const char* filename, double period);
/**
* Stop data log manager.
*/
void DLM_Stop(void);
/**
* Log a message to the "messages" entry. The message is also printed to
* standard output (followed by a newline).

View File

@@ -4,6 +4,7 @@ DLM_Log
DLM_LogNetworkTables
DLM_SignalNewDSDataOccur
DLM_Start
DLM_Stop
NT_AddListener
NT_AddListenerMultiple
NT_AddListenerSingle
@@ -245,6 +246,7 @@ WPI_DataLog_Resume
WPI_DataLog_SetFilename
WPI_DataLog_SetMetadata
WPI_DataLog_Start
WPI_DataLog_Stop
WPI_DestroyEvent
WPI_DestroySemaphore
WPI_DestroySignalObject

View File

@@ -24,6 +24,20 @@ if (WITH_JAVA)
endif()
endif()
if (WITH_JAVA_SOURCE)
find_package(Java REQUIRED)
include(UseJava)
file(GLOB_RECURSE ROMIVENDORDEP_SOURCES src/main/java/*.java)
add_jar(romiVendordep_src_jar
RESOURCES NAMESPACE "edu/wpi/first/wpilibj/romi" ${ROMIVENDORDEP_SOURCES}
OUTPUT_NAME romiVendordep-sources)
get_property(ROMIVENDORDEP_SRC_JAR_FILE TARGET romiVendordep_src_jar PROPERTY JAR_FILE)
install(FILES ${ROMIVENDORDEP_JAR_FILE} DESTINATION "${java_lib_dest}")
set_property(TARGET romiVendordep_src_jar PROPERTY FOLDER "java")
endif()
file(GLOB_RECURSE romiVendordep_native_src src/main/native/cpp/*.cpp)
add_library(romiVendordep ${romiVendordep_native_src})
set_target_properties(romiVendordep PROPERTIES DEBUG_POSTFIX "d")

View File

@@ -16,7 +16,7 @@ nativeUtils {
opencvYear = "frc2024"
googleTestYear = "frc2024"
niLibVersion = "2024.1.1"
opencvVersion = "4.8.0-1"
opencvVersion = "4.8.0-2"
googleTestVersion = "1.14.0-1"
}
}

View File

@@ -0,0 +1,7 @@
apply from: "${rootDir}/shared/cppDesktopTestTask.gradle"
apply from: "${rootDir}/shared/javaDesktopTestTask.gradle"
tasks.register('testDesktop') {
dependsOn testDesktopJava
dependsOn testDesktopCpp
}

View File

@@ -67,8 +67,8 @@ def tagList = [
"SmartDashboard", "Shuffleboard", "Sendable", "DataLog",
/* --- Controls --- */
"PID", "State-Space", "Ramsete", "Path Following", "Trajectory", "SysId",
"Simulation", "Trapezoid Profile", "Profiled PID", "Odometry", "LQR",
"Exponential Profile", "PID", "State-Space", "Ramsete", "Path Following", "Trajectory",
"SysId", "Simulation", "Trapezoid Profile", "Profiled PID", "Odometry", "LQR",
"Pose Estimator",
/* --- Hardware --- */
@@ -77,7 +77,7 @@ def tagList = [
"Digital Output",
/* --- HID --- */
"XboxController", "PS4Controller", "Joystick",
"XboxController", "PS4Controller", "PS5Controller", "Joystick",
/* --- Misc --- */
/* (try to keep this section minimal) */

View File

@@ -1,6 +1,7 @@
apply plugin: 'maven-publish'
apply plugin: 'java-library'
apply plugin: 'jacoco'
apply plugin: 'com.google.protobuf'
def baseArtifactId = project.baseId
def artifactGroupId = project.groupId
@@ -141,3 +142,27 @@ jacocoTestReport {
html.required = true
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.21.12'
}
plugins {
quickbuf {
artifact = 'us.hebi.quickbuf:protoc-gen-quickbuf:1.3.2'
}
}
generateProtoTasks {
all().configureEach { task ->
task.builtins {
cpp {}
remove java
}
task.plugins {
quickbuf {
option "gen_descriptors=true"
}
}
}
}
}

View File

@@ -30,11 +30,11 @@ model {
sources {
cpp {
source {
srcDirs 'src/main/native/cpp'
include '**/*.cpp'
srcDirs 'src/main/native/cpp', "$buildDir/generated/source/proto/main/cpp"
include '**/*.cpp', '**/*.cc'
}
exportedHeaders {
srcDirs 'src/main/native/include'
srcDirs 'src/main/native/include', "$buildDir/generated/source/proto/main/cpp"
}
}
}
@@ -43,6 +43,9 @@ model {
it.buildable = false
return
}
it.tasks.withType(CppCompile) {
it.dependsOn generateProto
}
if (project.hasProperty('extraSetup')) {
extraSetup(it)
}
@@ -179,8 +182,7 @@ model {
}
}
apply from: "${rootDir}/shared/cppDesktopTestTask.gradle"
apply from: "${rootDir}/shared/javaDesktopTestTask.gradle"
apply from: "${rootDir}/shared/cppJavaDesktopTestTask.gradle"
tasks.withType(RunTestExecutable) {
args "--gtest_output=xml:test_detail.xml"

View File

@@ -41,15 +41,15 @@ model {
sources {
cpp {
source {
srcDirs 'src/main/native/cpp'
srcDirs 'src/main/native/cpp', "$buildDir/generated/source/proto/main/cpp"
if (project.hasProperty('generatedSources')) {
srcDir generatedSources
}
include '**/*.cpp'
include '**/*.cpp', '**/*.cc'
exclude '**/jni/**/*.cpp'
}
exportedHeaders {
srcDir 'src/main/native/include'
srcDirs 'src/main/native/include', "$buildDir/generated/source/proto/main/cpp"
if (project.hasProperty('generatedHeaders')) {
srcDir generatedHeaders
}
@@ -67,6 +67,9 @@ model {
if (!project.hasProperty('noWpiutil')) {
lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
}
it.tasks.withType(CppCompile) {
it.dependsOn generateProto
}
if (project.hasProperty('splitSetup')) {
splitSetup(it)
}
@@ -324,8 +327,7 @@ model {
}
}
apply from: "${rootDir}/shared/cppDesktopTestTask.gradle"
apply from: "${rootDir}/shared/javaDesktopTestTask.gradle"
apply from: "${rootDir}/shared/cppJavaDesktopTestTask.gradle"
ext.getJniSpecClass = {
return JniNativeLibrarySpec

View File

@@ -1,4 +1,4 @@
def opencvVersion = '4.8.0-1'
def opencvVersion = '4.8.0-2'
if (project.hasProperty('useCpp') && project.useCpp) {
model {

View File

@@ -146,7 +146,9 @@ void DSCommPacket::DecodeUDP(std::span<const uint8_t> packet) {
m_lo = packet[1];
// Comm Version is packet 2, ignore
SetControl(packet[3], packet[4]);
SetAlliance(packet[5]);
// DS sends values 0, 1, and 2 for Red, but kUnknown is 0, so the value needs
// to be offset by one
SetAlliance(packet[5] + 1);
// Return if packet finished
if (packet.size() == 6) {

View File

@@ -16,6 +16,7 @@
#include <atomic>
#include <cstdio>
#include <cstring>
#include <exception>
#include <string_view>
#include <DSCommPacket.h>
@@ -124,33 +125,45 @@ static void SetupUdp(wpi::uv::Loop& loop) {
});
});
simLoopTimer->Start(Timer::Time{100}, Timer::Time{100});
// DS Timeout
int timeoutMs = 100;
if (auto envTimeout = std::getenv("DS_TIMEOUT_MS")) {
try {
timeoutMs = std::stoi(envTimeout);
} catch (const std::exception& e) {
fmt::print(stderr, "Error parsing DS_TIMEOUT_MS: {}\n", e.what());
}
}
auto autoDisableTimer = Timer::Create(loop);
autoDisableTimer->timeout.connect([] { HALSIM_SetDriverStationEnabled(0); });
// UDP Receive then send
udp->received.connect([udpLocal = udp.get()](Buffer& buf, size_t len,
const sockaddr& recSock,
unsigned int port) {
auto ds = udpLocal->GetLoop()->GetData<halsim::DSCommPacket>();
ds->DecodeUDP({reinterpret_cast<uint8_t*>(buf.base), len});
udp->received.connect(
[udpLocal = udp.get(), autoDisableTimer, timeoutMs](
Buffer& buf, size_t len, const sockaddr& recSock, unsigned int port) {
autoDisableTimer->Start(Timer::Time(timeoutMs));
auto ds = udpLocal->GetLoop()->GetData<halsim::DSCommPacket>();
ds->DecodeUDP({reinterpret_cast<uint8_t*>(buf.base), len});
struct sockaddr_in outAddr;
std::memcpy(&outAddr, &recSock, sizeof(sockaddr_in));
outAddr.sin_family = PF_INET;
outAddr.sin_port = htons(1150);
struct sockaddr_in outAddr;
std::memcpy(&outAddr, &recSock, sizeof(sockaddr_in));
outAddr.sin_family = PF_INET;
outAddr.sin_port = htons(1150);
wpi::SmallVector<wpi::uv::Buffer, 4> sendBufs;
wpi::raw_uv_ostream stream{sendBufs,
[] { return GetBufferPool().Allocate(); }};
ds->SetupSendBuffer(stream);
wpi::SmallVector<wpi::uv::Buffer, 4> sendBufs;
wpi::raw_uv_ostream stream{sendBufs,
[] { return GetBufferPool().Allocate(); }};
ds->SetupSendBuffer(stream);
udpLocal->Send(outAddr, sendBufs, [](auto bufs, Error err) {
GetBufferPool().Release(bufs);
if (err) {
fmt::print(stderr, "{}\n", err.str());
std::fflush(stderr);
}
});
ds->SendUDPToHALSim();
});
udpLocal->Send(outAddr, sendBufs, [](auto bufs, Error err) {
GetBufferPool().Release(bufs);
if (err) {
fmt::print(stderr, "{}\n", err.str());
std::fflush(stderr);
}
});
ds->SendUDPToHALSim();
});
udp->StartRecv();
}

View File

@@ -24,7 +24,7 @@ class HALSimWSClientConnection;
class HALSimWS : public std::enable_shared_from_this<HALSimWS> {
public:
using LoopFunc = std::function<void(void)>;
using LoopFunc = std::function<void()>;
using UvExecFunc = wpi::uv::Async<LoopFunc>;
HALSimWS(wpi::uv::Loop& loop, ProviderContainer& providers,

View File

@@ -87,7 +87,7 @@ class HALSimWSProviderSimDevice : public HALSimWSBaseProvider {
class HALSimWSProviderSimDevices {
public:
using LoopFn = std::function<void(void)>;
using LoopFn = std::function<void()>;
using UvExecFn = wpi::uv::AsyncFunction<void(LoopFn)>;
explicit HALSimWSProviderSimDevices(ProviderContainer& providers)

View File

@@ -22,7 +22,7 @@ namespace wpilibws {
class HALSimWeb : public std::enable_shared_from_this<HALSimWeb> {
public:
using LoopFunc = std::function<void(void)>;
using LoopFunc = std::function<void()>;
using UvExecFunc = wpi::uv::Async<LoopFunc>;
HALSimWeb(wpi::uv::Loop& loop, ProviderContainer& providers,

View File

@@ -22,7 +22,7 @@ namespace wpilibws {
class WebServerClientTest {
public:
using BufferPool = wpi::uv::SimpleBufferPool<4>;
using LoopFunc = std::function<void(void)>;
using LoopFunc = std::function<void()>;
using UvExecFunc = wpi::uv::AsyncFunction<void(LoopFunc)>;
explicit WebServerClientTest(wpi::uv::Loop& loop) : m_loop(loop) {}

View File

@@ -35,7 +35,7 @@ __declspec(dllexport)
return -1;
}
std::puts("HALSim XRP Extention Initialized");
std::puts("HALSim XRP Extension Initialized");
return 0;
}

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