Squashed 'third_party/allwpilib/' changes from e4b91005cf..83f1860047
83f1860047 [wpilib] Add/update documentation to PneumaticBase and subclasses (NFC) (#4881)
9872e676d8 [commands] Make Subsystem destructor virtual (#4892)
25db20e49d [hal] Fix segfault in various HAL functions (#4891)
b0c6724eed [glass] Add hamburger menu icon to titlebars (#4874)
f0fa8205ac Add missing compiler flags and fix warnings (#4889)
42fc4cb6bc [wpiutil] SafeThread: Provide start/stop hooks (#4880)
cc166c98d2 [templates] Add Command-based skeleton template (#4861)
3f51f10ad3 [build] Update to 2023v3 image (#4886)
1562eae74a [ntcore] Refactor meta-topic decoding from glass (#4809)
b632b288a3 Fix usages of std::min and std::max to be windows safe (#4887)
c11bd2720f [wpilibc] Add internal function to reset Shuffleboard instance (#4884)
f1151d375f [ntcore] Add method to get server time offset (#4847)
fe1b62647f [hal,wpilib] Update documentation for getComments (NFC) (#4879)
c49a45abbd [build] Fix examples linking in incorrect jni library (#4873)
bc3d01a721 [build] Add platform check to doxygen plugin (#4862)
bc473240ae Add Jetbrains Fleet folder to .gitignore (#4872)
2121bd5fb8 [wpimath] Remove RKF45 (#4870)
835f8470d6 [build] Fix roborio cross-compiler on arm hosts (#4864)
6cfe5de00d [ntcore] Don't deadlock server on early destroy (#4863)
2ac41f3edc [hal, wpilib] Add RobotController.getComments() (#4463)
26bdbf3d41 Java optimization and formatting fixes (#4857)
92149efa11 Spelling and grammar cleanups (#4849)
176fddeb4c [commands] Add functions to HID classes to allow use of axes as BooleanEvents/Triggers (#4762)
87a34af367 [templates] Add bindings to command-based template (#4838)
4534e75787 [examples] Remove redundant MotorControl example (#4837)
1cbebaa2f7 [commands] Remove final semicolon from test macro definition (#4859)
6efb9ee405 [commands] Add constructor for SwerveControllerCommand that takes a HolonomicDriveController (#4785)
1e7fcd5637 [cscore] Change run loop functions to not be mac specific (#4854)
1f940e2b60 [apriltag] Add C++ wrappers, rewrite Java/JNI to match (#4842)
a6d127aedf [build] Add missing task dependency in wpilibjExamples (#4852)
b893b3d6d3 [cscore] Add support for USB cameras on macOS (#4846)
1696a490fa [glass] Add support for alternate NT ports (#4848)
40a22d69bc [glass] Add support for alternate NT ports (#4848)
e84dbfede0 [wpilib] GenericHID: Add rumble both option (#4843)
8aa9dbfa90 [examples] Link apriltag package in examples build.gradle (#4845)
eda2fa8a17 [build] Update Spotless (#4840)
d20594db0d Fix typos (#4839)
dd8ecfdd54 [commands] Fix typo in waitUntil docs (NFC) (#4841)
17ceebfff4 [apriltag] Clean up apriltag JNI (#4823)
8b74ab389d [examples] RapidReactCommandBot: Fix array indices (#4833)
1aad3489c2 [sim] Implement PD total current and power (#4830)
2744991771 [wpimath] Fix docs in SwerveModulePosition (#4825)
ffbf6a1fa2 [commands] Disable regularly failing unit test (#4824)
fbabd0ef15 [commands] Enhance Command Sendable implementations (#4822)
7713f68772 [hal] Use atomic rather then mutex for DS Data updates (#4787)
701995d6cc [examples] Update Command-based starter project (#4778)
bf7068ac27 [wpilibc] Add missing PPS implementation for C++ (#4821)
aae0f52ca6 [ntcore] NetworkTable: fix visibility of get/set value (#4820)
ee02fb7ba7 [hal] Add support for Pulse-Per-Second signal (#4819)
518916ba02 [wpilib] Fix DS mode thread event being manual reset accidentally (#4818)
3997c6635b [hal] Update to new image, use new TCP notify callback and new duty cycle API (#4774)
cc8675a4e5 [examples] Add comment on how to view elevator sim (NFC) (#4482)
fb2c170b6e [ntcore] Simplify local startup (#4803)
7ba8a9ee1f [wpimath] ProfiledPIDController: Add to SendableRegistry (#4656)
c569d8e523 [wpilib] Joystick.getMagnitude(): use hypot() function (#4816)
2a5e89fa97 [apriltag] Improve description of pose coordinates (NFC) (#4810)
cc003c6c38 [apriltag] Fix AprilTagFieldLayout JSON name (#4814)
5522916123 [commands] CommandXBoxController bumper documentation fix (NFC) (#4815)
967b30de3a [glass] Fix NT view UpdateClients() bug (#4808)
3270d4fc86 [wpimath] Rewrite pose estimator docs (#4807)
be39678447 [apriltag] Add test to ensure apriltagjni loads (#4805)
61c75deb2a [commands] Test no-op behavior of scheduling a scheduled command (#4806)
a865f48e96 [ntcore] Pass pub/sub options as a unified PubSubOptions struct (#4794)
f66a667321 [commands] Fix incorrect Trigger docs (NFC) (#4792)
f8d4e9866e [ntcore] Clean up ntcore_test.h (#4804)
7e84ea891f [wpimath] Fix ComputerVisionUtil transform example in parameter docs (NFC) (#4800)
da3ec1be10 [wpimath] Change terminology for ArmFeedforward gravity gain (NFC) (#4791)
944dd7265d [wpilibc] Add C++ Notifier error handling, update java notifier error message (#4795)
6948cea67a [wpiutil] Fix MemoryBuffer initialization (#4797)
a31459bce6 [wpiutil] Fix UnescapeCString overflow when inputSize < 2 (#4796)
4a0ad6b48c [wpimath] Rotation2d: Add reference to angleModulus in docs (NFC) (#4786)
e6552d272e [ntcore] Remove table multi-subscriber (#4789)
bde383f763 [hal] Replace const char* with std::string_view in Driver Station sim functions (#4532)
5a52b51443 [hal] Add RobotController.getSerialNumber() (#4783)
69a66ec5ec [wpilib] Fix multiple motor safety issues (#4784)
989c9fb29a [wpimath] Revert Rotation2D change that limits angles (#4781)
0f5b08ec69 [wpigui] Update imgui to 1.89.1+ (#4780)
fba191099c [examples] AddressableLED: Add unit test (#4779)
b390cad095 [wpilibj] Consistently use ErrorMessages.requireNonNullParam (#4776)
b9772214d9 [wpilib] Sendable: Don't call setter for getter changes
342c375a71 [ntcore] Add subscriber option to exclude single publisher
b0e4053087 [ntcore] Use int for options instead of double
f3e666b7bb [cscore] Convert YUYV and UYVY directly to grayscale (#4777)
b300518bd1 [hal] Add CAN Stream API to Java through JNI bindings (#4193)
be27171236 [wpilibj] Shuffleboard: Check for null sendable (#4772)
4bbdbdfb48 [commands] Move GroupedCommands to CommandScheduler (#4728)
f18fd41ac3 [wpimath] Remove broken and obsoleted ComputerVisionUtil functions (#4775)
2d0faecf4f [glass] DataSource: Add spinlock to protect value (#4771)
348bd107fc [hal] Add CANManufacturer for The Thrifty Bot (#4773)
3149dc64b8 [examples] HatchbotInlined: Use Subsystem factories (#4765)
8618dd4160 [glass, wpilib] Replace remaining references to Speed Controller with Motor Controller (#4769)
72e21a1ed1 [apriltag] Use wpilibsuite fork of apriltag (#4764)
eab0d929e6 [commands] CommandGenericHID POV methods: Fix docs (NFC) (#4760)
6789869663 [wpilib] Call set(0) rather than disable for stopMotor (#4763)
c9dea2968d [cscore] Emit warning that USB Camera isn't supported on OSX (#4766)
8f402645f5 [commands] Fix PIDSubsystem setSetpoint behavior (#4759)
f24ad1d715 [build] Upgrade to googletest 1.12.1 (#4752)
ff88756864 [wpimath] Add new DCMotor functions for alternative calculations and reduction calculation (#4749)
f58873db8e [wpimath] Remove extra terms in matrix for pose estimator docs (#4756)
37e969b41a [wpimath] Add constructors to pose estimators with default standard deviations (#4754)
13cdc29382 [ci] Rename comment command from "/wpiformat" to "/format" (#4755)
6e23985ae6 [examples] Add main include directory to test builds (#4751)
66bb0ffb2c [examples] Add unit testing infrastructure (#4646)
74cc86c4c5 [wpimath] Make transform tests use pose/transform equality operators (#4675)
e22d8cc343 [wpimath] Use Odometry for internal state in Pose Estimation (#4668)
68dba92630 [ci] Update mac and windows builds to Java 17 (#4750)
23bfc2d9ab [sim] Remove unmaintained Gazebo support (#4736)
1f1461e254 [wpilib] Add method to enable/disable LiveWindow in test mode (#4678)
eae68fc165 [wpimath] Add tolerance for Rotation3d rotation matrix special orthogonality (#4744)
4c4545fb4b [apriltag] Suppress warning (#4743)
16ffaa754d [docs] Generate docs for apriltag subproject (#4745)
5e74ff26d8 [apriltag, build] Update native utils, add apriltag impl and JNI (#4733)
53875419a1 [hal] Allow overriding stderr printing by HAL_SendError (#4742)
aa6499e920 [ntcore] Fix special topic multi-subscriber handling (#4740)
df70351107 [build] Fix cmake install of thirdparty includes (#4741)
e9bd50ff9b [glass] NT view: clear meta-topic info on disconnect (#4732)
9b319fd56b [ntcore] Add sub option for local vs remote changes (#4731)
18d28ec5e3 [ntcore] Remove duplicate value checking from ClientImpl
bdfb625211 [ntcore] Send duplicate values to network if necessary
21003e34eb [commands] Update Subsystem factories and example to return CommandBase (#4729)
70080457d5 [commands] Refactor ProxyScheduleCommand, SelectCommand into ProxyCommand (#4534)
e82cd5147b [wpilib] Tweak Color HSV formula and use in AddressableLED (#4724)
ec124bb662 [commands] Allow unsetting a subsystem's default command (#4621)
2b2aa8eef7 [examples] Update all examples to use NWU coordinate conventions (#4725)
cb38bacfe8 [commands] Revert to original Trigger implementation (#4673)
15561338d5 [commands] Remove one more default command isFinished check (#4727)
ca35a2e097 Add simgui files to .gitignore (#4726)
20dbae0cee [examples] Renovate command-based examples (#4409)
1a59737f40 [commands] Add convenience factories (#4460)
42b6d4e3f7 Use defaulted comparison operators in C++ (#4723)
135c13958f [wpigui] Add FontAwesome (#4713)
ffbfc61532 [ntcore] Add NetworkTable table-specific listeners (#4640)
8958b2a4da [commands] Add property tests for command compositions (#4715)
e4ac09077c [wpilib] Add link to MotorSafety article (#4720)
f40de0c120 [commands] Add C++ factory templates (#4686)
51fa3e851f [build] cmake: Use FetchContent instead of ExternalProject (#4714)
1da84b2255 [wpigui] Reload fonts to scale rather than preloading (#4712)
e43e2fbc84 [wpiutil] StringExtras: Add UnescapeCString (#4707)
5804d8fa84 [ntcore] Server: Properly handle multiple subscribers (#4717)
169ef5fabf [glass] Update NT view for topicsOnly and sendAll changes (#4718)
148759ef54 [examples] CANPDP: Expand properties shown (#4687)
58ed112b51 [commands] RepeatCommand: restart on following iteration (#4706)
dd1da77d20 [readme] Fix broken CI badge (#4710)
7cda85df20 [build] Check Gradle plugin repo last to fix CI (#4711)
7ed9b13277 [build] Bump version plugin to fix null tag (#4705)
6b4f26225d [apriltag] Fix pluralization of apriltag artifacts (#4671)
b2d2924b72 [cscore] Add Y16 image support (#4702)
34ec89c041 [wpilibc] Shuffleboard SimpleWidget: Return pointer instead of reference (#4703)
e15200068d [ci] Disable HW testbench runs (#4704)
d5200db6cd [wpimath] Rename HolonomicDriveController.calculate params (#4683)
2ee3d86de4 [wpimath] Clarify Rotation3d roll-pitch-yaw direction (#4699)
9f0a8b930f [cscore] Use MFVideoFormat_L8 for Gray on Windows (#4701)
2bca43779e [cscore] Add UYVY image support (#4700)
4307d0ee8b [glass] Plot: allow for more than 11 plots (#4685)
3fe8d355a1 [examples] StateSpaceDifferentialDriveSimulation: Use encoder reversed constants (#4682)
b44034dadc [ntcore] Allow duplicate client IDs on server (#4676)
52d2c53888 [commands] Rename Java factory wait() to waitSeconds() (#4684)
76e918f71e [build] Fix JNI artifacts linking to incorrect libraries (#4680)
0bee875aff [commands] Change C++ CommandPtr to use CommandBase (#4677)
98e922313b [glass] Don't check IsConnected for NT widgets (#4674)
9a36373b8f [apriltag] Switch 2022 apriltag layout length and width values (#4670)
cf8faa9e67 [wpilib] Update values on controllable sendables (#4667)
5ec067c1f8 [ntcore] Implement keep duplicates pub/sub flag (#4666)
e962fd2916 [ntcore] Allow numeric-compatible value sets (#4620)
88bd67e7de [ci] Update clang repositories to jammy (#4665)
902e8686d3 [wpimath] Rework odometry APIs to improve feature parity (#4645)
e2d49181da Update to native utils 2023.8.0 (#4664)
149bac55b1 [cscore] Add Arducam OV9281 exposure quirk (#4663)
88f7a3ccb9 [wpimath] Fix Pose relativeTo documentation (#4661)
8acce443f0 [examples] Fix swerve examples to use getDistance for turning encoder (#4652)
295a1f8f3b [ntcore] Fix WaitForListenerQueue (#4662)
388e7a4265 [ntcore] Provide mechanism to reset internals of NT instance (#4653)
13aceea8dc [apriltag] Fix FieldDimensions argument order (#4659)
c203f3f0a9 [apriltag] Fix documentation for AprilTagFieldLayout (#4657)
f54d495c90 Fix non initialized hal functionality during motor safety init (#4658)
e6392a1570 [cmd] Change factories return type to CommandBase (#4655)
53904e7cf4 [apriltag] Split AprilTag functionality to a separate library (#4578)
2e88a496c2 [wpimath] Add support for swerve joystick normalization (#4516)
ce4c45df13 [wpimath] Rework function signatures for Pose Estimation / Odometry (#4642)
0401597d3b [readme] Add wpinet to MavenArtifacts.md (#4651)
2e5f9e45bb [wpimath] Remove encoder reset comments on Swerve, Mecanum Odometry and Pose Estimation (#4643)
e4b5795fc7 [docs] Disable Doxygen for memory to fix search (#4636)
03d0ea188c [build] cmake: Add missing wpinet to installed config file (#4637)
3082bd236b [build] Move version file to its own source set (#4638)
b7ca860417 [build] Use build cache for sign step (#4635)
64838e6367 [commands] Remove unsafe default command isFinished check (#4411)
1269d2b901 [myRobot] Disable spotbugs (#4565)
14d8506b72 [wpimath] Fix units docs for LinearSystemId::IdentifyDrivetrainSystem() (#4600)
d1d458db2b [wpimath] Constrain Rotation2d range to -pi to pi (#4611)
f656e99245 [readme] Add links to development build documentation (#4481)
6dd937cef7 [commands] Fix Trigger API docs (NFC) (#4599)
49047c85b9 [commands] Report error on C++ CommandPtr use-after-move (#4575)
d07267fed1 [ci] Upgrade containers to Ubuntu 22.04 and remove libclang installation (#4633)
b53ce1d3f0 [build, wpiutil] Switch macos to universal binaries (#4628)
5a320c326b [upstream_util, wpiutil] Refactor python scripts (#4614)
c4e526d315 [glass] Fix NT Mechanism2D (#4626)
d122e4254f [ci] Run spotlessApply after wpiformat in comment command (#4623)
5a1e7ea036 [wpilibj] FieldObject2d: Add null check to close() (#4619)
179f569113 [ntcore] Notify locally on SetDefault (#4617)
b0f6dc199d [wpilibc] ShuffleboardComponent.WithProperties: Update type (#4615)
7836f661cd [wpimath] Add missing open curly brace to units/base.h (#4613)
dbcc1de37f [wpimath] Add DifferentialDriveFeedforward classes which wrap LinearPlantInversionFeedforward (#4598)
93890c528b [wpimath] Add additional angular acceleration units (#4610)
3d8d5936f9 [wpimath] Add macro for disabling units fmt support (#4609)
2b04159dec [wpimath] Update units/base.h license header (#4608)
2764004fad [wpinet] Fix incorrect jni definitions (#4605)
85f1bb8f2b [wpiutil] Reenable jni check task (#4606)
231ae2c353 [glass] Plot: Fix Y-axis not being saved (#4594)
e92b6dd5f9 [wpilib] Fix AprilTagFieldLayout JSON property name typos (#4597)
2a8e0e1cc8 Update all dependencies that use grgit (#4596)
7d06e517e9 [commands] Move SelectCommand factory impl to header (#4581)
323524fed6 [wpimath] Remove deprecated units/units.h header (#4572)
d426873ed1 [commands] Add missing PS4 triangle methods (#4576)
5be5869b2f [apriltags] Use map as internal data model (#4577)
b1b4c1e9e7 [wpimath] Fix Pose3d transformBy rotation type (#4545)
a4054d702f [commands] Allow composing two triggers directly (#4580)
0190301e09 [wpilibc] Explicitly mark EventLoop as non-copyable/non-movable (#4579)
9d1ce6a6d9 [ntcore] Catch file open error when saving preferences (#4571)
5005e2ca04 [ntcore] Change Java event mask to EnumSet (#4564)
fa44a07938 [upstream-utils][mpack] Add upstream util for mpack (#4500)
4ba16db645 [ntcore] Various fixes and cleanups (#4544)
837415abfd [hal] Fix joysticks either crashing or returning 0 (#4570)
2c20fd0d09 [wpilib] SingleJointedArmSim: Check angle equals limit on wouldHit (#4567)
64a7136e08 [wpimath] SwerveDrivePoseEstimator: Restore comment about encoder reset (#4569)
b2b473b24a [wpilib] Add AprilTag and AprilTagFieldLayout (#4421)
7aab8fa93a [build] Update to Native Utils 2023.6.0 (#4563)
12c2851856 [commands] WrapperCommand: inherit from CommandBase (#4561)
0da169dd84 [wpimath] Remove template argument from ElevatorFeedforward (#4554)
2416827c25 [wpimath] Fix docs for pose estimator local measurement models (#4558)
1177a3522e [wpilib] Fix Xbox/PS4 POV sim for port number constructors (#4548)
102344e27a [commands] HID classes: Add missing methods, tweak return types (#4557)
1831ef3e19 [wpilib] Fix Shuffleboard SuppliedValueWidget (#4559)
a9606ce870 [wpilib] Fix Xbox/PS4 POV sim (#4546)
6c80d5eab3 [wpimath] Remove unused SymbolExports.h include from units/base.h (#4541)
b114006543 [ntcore] Unify listeners (#4536)
32fbfb7da6 [build] cmake: Install ntcore generated include files (#4540)
02465920fb [build] Update native utils to 2023.4.0 (#4539)
3a5a376465 [wpimath] Increase constexpr support in geometry data types (#4231)
1c3c86e9f1 [ntcore] Cache GetEntry(name) values (#4531)
dcda09f90a [command] Rename trigger methods (#4210)
66157397c1 [wpilib] Make drive classes follow NWU axes convention (#4079)
9e22ffbebf [ntcore] Fix null deref in NT3 client (#4530)
648ab6115c [wpigui,dlt,glass,ov] Support arm in GUI tools (#4527)
8bc3b04f5b [wpimath] Make ComputerVisionUtil use 3D geometry classes (#4528)
cfb84a6083 [wpilibc] Don't hang waiting for NT server to start (#4524)
02c47726e1 [wpimath] Remove unused odometry instance from DifferentialDrivePoseEstimator test (#4522)
b2a0093294 [ci] Revert upgrade of github-pages-deploy-action (#4521)
2a98d6b5d7 [wpimath] PIDController: Add getters for position & velocity tolerances (#4458)
9f36301dc8 [ci] Write wpiformat patch to job summary (#4519)
901fc555f4 [wpimath] Position Delta Odometry for Mecanum (#4514)
4170ec6107 [wpimath] Position Delta Odometry for Swerve (#4493)
fe400f68c5 [docs] Add wpinet to docs build (#4517)
794669b346 [ntcore] Revamp listeners (#4511)
dcfa85a5d5 [ci] Build sanitizers with clang-14 (#4518)
15ad855f1d [ntcore] Add UnitTopic<T> (C++ only) (#4497)
11244a49d9 [wpilib] Add IsConnected function to all gyros (#4465)
1d2e8eb153 [build] Update myRobot deployment (#4515)
ad53fb19b4 [hal] Use new HMB api for addressable LED (#4479)
ba850bac3b [hal] Add more shutdown checks and motor safety shutdown (#4510)
023a5989f8 [ntcore] Fix typo in NetworkServer client connect message (#4512)
c970011ccc [docs] Add Doxygen aliases used by Foonathan memory (#4509)
07a43c3d9a [readme] Document clang-format version and /wpiformat (#4503)
a05b212b04 [ci] Revert changes to wpiformat task from #4501 (#4508)
09faf31b67 [commands] Replace Command HID inheritance with delegation (#4470)
9e1f9c1133 [commands] Add command factories (#4476)
f19d2b9b84 [ci] Add NUMBER environment variable to comment command commit script (#4507)
a28f93863c [ci] Push comment command commit directly to PR (#4506)
c9f61669b8 [ci] Fix comment command commit push (#4505)
dcce5ad3b3 [ci] Update github-script API usage (#4504)
6836e5923d [wpilibc] Restore get duty cycle scale factor (#4502)
335188c652 [dlt] Add deselect/select all buttons to download view (#4499)
60a29dcb99 [glass] Field2D: Add "hidden" option for objects (#4498)
b55d5b3034 [ci] Update deprecated github actions (#4501)
10ed4b3969 [ntcore] Various NT4 fixes (#4474)
4a401b89d7 [hal, wpilib] New DS thread model and implementation (#3787)
c195b4fc46 [wpimath] Clean up PoseEstimator nominal dt docs (#4496)
8f2e34c6a3 [build] Remove wpilib prefix from CMake flat install (#4492)
150d692df7 [wpimath] Remove unused private PoseEstimator function (#4495)
3e5bfff1b5 [wpimath] FromFieldRelativeSpeeds: Add ChassisSpeeds overload (#4494)
9c7e66a27d [commands] C++: Add CommandPtr overload for SetDefaultCommand (#4488)
0ca274866b [build] Fix CMake system library opt-ins (#4487)
dc037f8d41 [commands] Remove EndlessCommand (#4483)
16cdc741cf [wpimath] Add Pose3d(Pose2d) constructor (#4485)
9d5055176d [build] cmake: Allow disabling ntcore build (#4486)
d1e66e1296 [build] Compile all java code with inline string concatenation (#4490)
1fc098e696 Enable log macros to work with no args (#4475)
878cc8defb [wpilib] LiveWindow: Add enableAllTelemetry() (#4480)
8153911160 [build] Fix MSVC runtime archiver to grab default runtime (#4478)
fbdc810887 Upgrade to C++20 (#4239)
396143004c [ntcore] Add ntcoreffi binary (#4471)
1f45732700 [build] Update to 2023.2.4 native-utils and new dependencies (#4473)
574cb41c18 [ntcore] Various fixes (#4469)
d9d6c425e7 [build] Force Java 11 source compatibility (#4472)
58b6484dbe Switch away from NI interrupt manager to custom implementation (#3705)
ca43fe2798 [wpimath] Use Units conversions in ComputerVisionUtil docs (NFC) (#4464)
87a64ccedc [hal] Convert DutyCycle Raw output to be a high time measurement (#4466)
89a3d00297 [commands] Add FinallyDo and HandleInterrupt decorators (#4412)
1497665f96 [commands] Add C++ versions of Java-only decorators (#4457)
27b173374e [wpimath] Add minLinearAccel parameter to DifferentialDriveAccelerationLimiter (#4422)
2a13dba8ac [wpilib] TrajectoryUtil: Fix ambiguous documentation (NFC) (#4461)
77301b126c [ntcore] NetworkTables 4 (#3217)
90cfa00115 [build] cmake: Fix libssh include directory order (#4459)
5cf961edb9 [commands] Refactor lambda-based commands to inherit FunctionalCommand (#4451)
b2276e47de [wpimath] Enable continuous angle input for HolonomicDriveController (#4453)
893b46139a [fieldImages] Add utilities to simplify loading of fields (#4456)
60e29627c0 [commands] C++ unique_ptr migration (#4319)
3b81cf6c35 [wpilib] Improve Color.toString (#4450)
5c067d30a0 [wpinet] WebSocket: Add SendFrames() (#4445)
ceaf493811 [wpiutil] MakeJByteArray: Use span<uint8> instead of string_view (#4446)
10e04e2b13 [examples] FrisbeeBot: Fix reference capture (#4449)
726f67c64b [build] Add exeSplitSetup (#4444)
c7b7624c1c [wpiutil] Add MessagePack utility functions (#4448)
d600529ec0 [wpinet] uv::Async: Add UnsafeSend() (#4447)
b53b3526a2 [wpimath] Add CoordinateSystem conversion for Transform3d (#4443)
38bb23eb18 [wpimath] Add scalar multiply and divide operators to all geometry classes (#4438)
3937ff8221 [wpilib] Remove deprecated Controller class (#4440)
abbfe244b5 [wpilib] Improve Color FromHSV (#4439)
4ddb8aa0dd [sim] Provide function that resets all simulation data (#4016)
a791470de7 Clean up Java warning suppressions (#4433)
17f504f548 [hal,wpilib] Fix SPI Mode Setting (#4434)
773198537c [wpiutil] Add wpi::scope_exit (#4432)
5ac658c8f0 [wpiutil] Logger: Conditionalize around WPI_LOG (#4431)
8767e4a941 [wpiutil] DataLog: Fix SetMetadata output (#4430)
8c4af073f4 [wpiutil] Synchronization: shutdown race protection (#4429)
c79f38584a [build] Fix Java integration tests (#4428)
36c08dd97c [build] Fix cmake install of fmtlib (#4426)
69b7b3dd7d [ci] Remove the Windows cmake job (#4425)
738c75fed8 [readme] Fix formatting/linting link (#4423)
4eb1d03fb3 [wpimath] Document C++ LinearFilter exception (#4417)
ba4ec6c967 [build] Fix clang-tidy false positive on Linux (#4406)
97836f0e55 [commands] Fix ProfiledPIDSubsystem setGoal behavior (#4414)
fdfb85f695 [wpimath] Remove Java LQR constructor that takes a controller gain matrix (#4419)
ab1baf4832 [wpimath] Add rotation matrix constructor to Rotation3d (#4413)
9730032866 [wpimath] Document LQR and KalmanFilter exceptions (#4418)
5b656eecf6 [wpimath] Fix HTML5 entity (#4420)
9ae38eaa7c [commands] Add owning overload to ProxyScheduleCommand (#4405)
cb33bd71df [commands] deprecate withInterrupt decorator (#4407)
d9b4e7b8bf [commands] Revert "Change grouping decorator impl to flatten nested group structures (#3335)" (#4402)
0389bf5214 [hal] REVPH: Improve handling of disconnected CAN Bus (#4169)
4267fa08d1 [wpilibc] ADIS IMUs: Fix memory leak (#4170)
65c8fbd452 [wpilib] MotorControllerGroup: Override setVoltage (#4403)
f36162fddc [wpimath] Improve Discretization internal docs (#4400)
5149f7d894 [wpimath] Add two-vector Rotation3d constructor (#4398)
20b5bed1cb [wpimath] Clean up Java Quaternion class (#4399)
f18dd1905d [build] Include all thirdparty sources in distribution (#4397)
aa9d7f1cdc [wpiutil] Import foonathan memory (#4306)
2742662254 [ci] Remove a couple of obsolete clang-tidy checks (#4396)
a5df391166 [hal, wpilib] Fix up DIO pulse API (#4387)
59e6706b75 [glass] Turn on docking by default
8461bb1e03 [glass] Add support for saving docking info
b873e208b4 [wpigui] Add support for imgui config flags
873e72df8c [build] Update imgui to 1.88 docking branch
c8bd6fc5b4 [ci] Fix comment-command (take 2) (#4395)
fed68b83b4 [ci] Fix comment-command action not running runners (#4393)
0ef8a4e1df [wpimath] Support formatting more Eigen types (#4391)
c393b3b367 [build] Update to native utils 2023.1.0 and Gradle 7.5.1 (#4392)
b5a17f762c [wpimath] Add direction to slew rate limiter (#4377)
fafc81ed1a [wpiutil] Upgrade to fmt 9.1.0 (#4389)
cc56bdc787 [wpiutil] SafeThread: Add Synchronization object variant (#4382)
4254438d8d [commands] Mark command group lifecycle methods as final (#4385)
97c15af238 [wpimath] LinearSystemId: Fix docs, move C++ impls out of header (#4388)
d22ff8a158 [wpiutil] Add JNI access to C++ stderr (#4381)
fdb5a2791f [wpiutil] jni_util: Add Mac-friendly MakeJLongArray/JArrayRef (#4383)
c3a93fb995 [commands] Revamp Interruptible (#4192)
f2a8d38d2a [commands] Rename Command.repeat to repeatedly (#4379)
9e24c6eac0 [wpiutil] Logger: paren-protect instance usage in macro (#4384)
fe4d12ce22 [wpimath] Add LTV controller derivations and make enums private (#4380)
eb08486039 [build] Fix MacOS binary rpath generation (#4376)
ccf83c634a [build] Use native-utils platform names instead of raw strings (#4375)
3fd69749e7 [docs] Upgrade to doxygen 1.9.4 (#4370)
594df5fc08 [wpinet] uv/util.h: Pull in ws2_32.lib on Windows for ntohs (#4371)
539070820d [ci] Enable asan for wpinet and wpiutil (#4369)
564a56d99b [wpinet] Fix memory leak in WorkerThreadTest (#4368)
5adf50d93c [upstream_utils] Refactor upstream_utils scripts (#4367)
d80e8039d7 [wpiutil] Suppress fmtlib clang-tidy warning in C++20 consteval contexts (#4364)
0e6d67b23b [upstream_utils] Remove yapf format disable comment (#4366)
be5270697a [build] Suppress enum-enum deprecation warning in OpenCV (#4365)
8d28851263 Add Rosetta install command to build requirements (#4363)
3d2115c93e [wpinet] include-what-you-use in MulticastTest (#4360)
91002ae3cc [wpimath] Upgrade to Drake 1.6.0 (#4361)
148c18e658 [wpinet] Upgrade to libuv 1.44.2 (#4362)
a2a5c926b6 Fix clang-tidy warnings (#4359)
ea6b1d8449 [wpiutil] Remove unused ManagedStatic class (#4358)
ac9be78e27 Use stricter C++ type conversions (#4357)
151dabb2af [wpiutil] Upgrade to fmt 9.0.0 (#4337)
340465c929 [ci] Upgrade to clang-format and clang-tidy 14 (NFC) (#4347)
d45bcddd15 [examples] Add comments to StateSpaceDifferentialDrive (#4341)
0e0786331a Update LLVM libraries to 14.0.6 (#4350)
c5db23f296 [wpimath] Add Eigen sparse matrix and iterative solver support (#4349)
44abc8dfa6 [upstream_utils] Remove git version from upstream patches (#4351)
3fdb2f767d [wpimath] Add comments with Ramsete equations (#4348)
0485f05da9 [wpilibjExamples] Upgrade jacoco to match allwpilib (#4346)
0a5eb65231 [wpinet] Handle empty txt block for mdns announcer (#4072)
19ffebaf3e [wpilib] Add reference to I2C Lockup to API Docs (NFC) (#4340)
ce1a90d639 [hal] Replace SerialHelper "goto done" with continue (#4342)
d25af48797 [ci] Make upstream_utils CI fail on untracked files (#4339)
ebb836dacb [examples] Fix negations in event loop examples (#4334)
d83e202f00 [upstream_utils] Update paths in update_fmt.py (#4338)
3ccf806064 [wpimath] Remove redundant LinearFilter.finiteDifference() argument (#4335)
6f1e01f8bd [wpimath] Document example of online filtering for LinearFilter.finiteDifference() (#4336)
1023c34b1c [readme] Update location of ni-libraries (#4333)
faa29d596c [wpilib] Improve Notifier docs (NFC) (#4326)
add00a96ed [wpimath] Improve DifferentialDriveAccelerationLimiter docs (NFC) (#4323)
82fac41244 [wpimath] Better document trackwidth parameters (NFC) (#4324)
5eb44e22a9 Format Python scripts with black (NFC) (#4325)
2e09fa7325 [build] Fix mpack cmake (#4322)
fe3c24b1ee [command] Add ignoringDisable decorator (#4305)
aa221597bc [build] Add M1 builds, change arm name, update to 2023 deps (#4315)
579a8ee229 [ci] Use one worker for Windows release Gradle build (#4318)
5105c5eab6 [wpilibj] Change "final" to "exit" in the IterativeRobotBase JavaDoc (NFC) (#4317)
787fe6e7a5 [wpiutil] Separate third party libraries (#4190)
6671f8d099 [wpigui] Update portable file dialogs (#4316)
9ac9b69aa2 [command] Reorder Scheduler operations (#4261)
e61028cb18 [build] halsim_gui: Add wpinet dependency (#4313)
661d23eaf5 [glass] Add precision setting for NetworkTable view (#4311)
666040e3e5 [hal] Throw exceptions for invalid sizes in I2C and SPI JNI (#4312)
aebc272449 [build] Upgrade to spotbugs Gradle plugin 5.0.8 (#4310)
fd884581e4 [wpilib] Add BooleanEvent/Trigger factories on HID classes (#4247)
9b1bf5c7f1 [wpimath] Move Drake and Eigen to thirdparty folders (#4307)
c9e620a920 [wpilibc] Change EventLoop data structure to vector (#4304)
41d40dd62f [wpinet] Fix libuv unused variable warning on Mac (#4299)
30f5b68264 [wpinet] Fix JNI loading error (#4295)
f7b3f4b90e [examples] Getting Started: Change Joystick to XboxController (#4194)
a99c11c14c [wpimath] Replace UKF implementation with square root form (#4168)
45b7fc445b [wpilib] Add EventLoop (#4104)
16a4888c52 [wpilib] Default off LiveWindow telemetry (#4301)
17752f1337 [ci] Split debug and release Windows builds (#4277)
abb45a68db [commands] Remove custom test wrappers (#4296)
1280a54ef3 [upstream_utils]: Make work with Python 3.8 (#4298)
f2d243fa68 [build] Change defaults for Java lints (#4300)
a4787130f4 Update using development build to work with 2023 gradlerio (#4294)
af7985e46c [wpiutil] Use invoke_result_t instead of result_of in future.h (#4293)
e9d1b5c2d0 [hal] Remove deprecated SimDevice functions (#4209)
45b598d236 [wpilibj] Add toString() methods to Color and Color8Bit (#4286)
fc37265da5 [wpimath] Add angle measurement convention to ArmFeedforward docs (NFC) (#4285)
a4ec13eb0e [wpilibjexamples] Remove unnecessary voltage desaturation
2fa52007af [wpilibc] Use GetBatteryVoltage() in MotorController::SetVoltage
d9f9cd1140 [wpimath] Reset prev_time on pose estimator reset (#4283)
8b6df88783 [wpilibj] Tachometer.getFrequency(): Fix bug (#4281)
345cff08c0 [wpiutil] Make wpi::array constexpr (#4278)
57428112ac [wpimath] Upgrade to Drake v1.3.0 (#4279)
a18d4ff154 [build] Fix tools not being copied when built with -Ponly* (#4276)
d1cd07b9f3 [wpigui] Add OpenURL (#4273)
e67f8e917a [glass] Use glfwSetKeyCallback for Enter key remap (#4275)
be2fedfe50 [wpimath] Add stdexcept include for std::invalid_argument (IWYU) (#4274)
7ad2be172e [build] Update native-utils to 2023.0.1 (#4272)
abc605c9c9 [ci] Update workflows to 20.04 base image (#4271)
3e94805220 [wpiutil] Reduce llvm collections patches (#4268)
db2e1d170e [upstream_utils] Document how to update thirdparty libraries (#4253)
96ebdcaf16 [wpimath] Remove unused Eigen AutoDiff module (#4267)
553b2a3b12 [upstream_utils] Fix stackwalker (#4265)
3e13ef42eb [wpilibc] Add missing std::array #include (include-what-you-use) (#4266)
d651a1fcec Fix internal deprecation warnings (#4257)
b193b318c1 [commands] Add unless() decorator (#4244)
ef3714223b [commands] Remove docs reference to obsolete interrupted() method (NFC) (#4262)
3d8dbbbac3 [readme] Add quickstart (#4225)
013efdde25 [wpinet] Wrap a number of newer libuv features (#4260)
816aa4e465 [wpilib] Add Pneumatics sim classes (#4033)
046c2c8972 [wpilibc] Rename SpeedControllerGroupTest.cpp (#4258)
d80e9cdf64 [upstream_utils] Use shallow clones for thirdparty repos (#4255)
7576136b4a [upstream_utils] Make update_llvm.py executable (#4254)
c3b223ce60 [wpiutil] Vendor llvm and update to 13.0.0 (#4224)
5aa67f56e6 [wpimath] Clean up math comments (#4252)
fff4d1f44e [wpimath] Extend Eigen warning suppression to GCC 12 (#4251)
0d9956273c [wpimath] Add CoordinateSystem.convert() translation and rotation overloads (#4227)
3fada4e0b4 [wpinet] Update to libuv 1.44.1 (#4232)
65b23ac45e [wpilibc] Fix return value of DriverStation::GetJoystickAxisType() (#4230)
4ac34c0141 [upstream_utils] Cleanup update_libuv.py (#4249)
8bd614bb1e [upstream_utils] Use "git am" instead of "git apply" for patches (#4248)
4253d6d5f0 [upstream_utils] Apply "git am" patches individually (#4250)
6a4752dcdc Fix GCC 12.1 warning false positives (#4246)
5876b40f08 [wpimath] Memoize CoordinateSystem and CoordinateAxis statics (#4241)
5983434a70 [cameraserver] Replace IterativeRobot in comment sample code with TimedRobot (#4238)
a3d44a1e69 [wpimath] Add Translation2d.getAngle() (#4217)
d364bbd5a7 [upstream_utils] Give vendor update scripts execute permissions (#4226)
f341e1b2be [wpimath] Document standard coordinate systems better (NFC) (#4228)
9af389b200 [wpinet] AddrToName: Initialize name (#4229)
2ae4adf2d7 [ci] Add wpiformat command to PRs (#4223)
178b2a1e88 Contributing.md: Correct version of clang-format used (#4222)
18db343cdc [wpiutil, wpinet] Vendor libuv, stack walker (#4219)
f0c821282a [build] Use artifactory mirror (#4220)
d673ead481 [wpinet] Move network portions of wpiutil into new wpinet library (#4077)
b33715db15 [wpimath] Add CoordinateSystem class (#4214)
99424ad562 [sim] Allow creating a PWMSim object from a PWMMotorController (#4039)
dc6f641fd2 [wpimath] PIDController: Reset position and velocity error when reset() is called. (#4064)
f20a20f3f1 [wpimath] Add 3D geometry classes (#4175)
708a4bc3bc [wpimath] Conserve previously calculated swerve module angles when updating states for stationary ChassisSpeeds (#4208)
ef7ed21a9d [wpimath] Improve accuracy of ComputerVisionUtil.calculateDistanceToTarget() (#4215)
b1abf455c1 [wpimath] LTVUnicycleController: Use LUT, provide default hyperparameters (#4213)
d5456cf278 [wpimath] LTVDifferentialDriveController: Remove unused variable (#4212)
99343d40ba [command] Remove old command-based framework (#4211)
ee03a7ad3b Remove most 2022 deprecations (#4205)
ce1a7d698a [wpimath] Refactor WheelVoltages inner class to a separate file (#4203)
87bf70fa8e [wpimath] Add LTV controllers (#4094)
ebd2a303bf [wpimath] Remove deprecated MakeMatrix() function (#4202)
e28776d361 [wpimath] LinearSystemLoop: Add extern templates for common cases
dac1429aa9 [wpimath] LQR: Use extern template instead of Impl class
e767605e94 [wpimath] Add typedefs for common types
97c493241f [wpimath] UnscentedKalmanFilter: Move implementation out-of-line
8ea90d8bc9 [wpimath] ExtendedKalmanFilter: Move implementation out-of-line
ae7b1851ec [wpimath] KalmanFilter: Use extern template instead of Impl class
e3d62c22d3 [wpimath] Add extern templates for common cases
7200c4951d [wpiutil] SymbolExports: Add WPILIB_IMPORTS for dllimport
84056c9347 [wpiutil] SymbolExports: Add EXPORT_TEMPLATE_DECLARE/DEFINE
09cf6eeecb [wpimath] ApplyDeadband: add a scale param (#3865)
03230fc842 [build,ci] Enable artifactory build cache (#4200)
63cf3aaa3f [examples] Don't square ArcadeDrive inputs in auto (#4201)
18ff694f02 [wpimath] Add Rotation2d.fromRadians factory (#4178)
4f79ceedd9 [wpilibc] Add missing #include (#4198)
f7ca72fb41 [command] Rename PerpetualCommand to EndlessCommand (#4177)
a06b3f0307 [hal] Correct documentation on updateNotifierAlarm (#4156)
d926dd1610 [wpimath] Fix pose estimator performance (#4111)
51bc893bc5 [wpiutil] CircularBuffer: Change Java package-private methods to public (#4181)
fbe761f7f6 [build] Increase Gradle JVM heap size (#4172)
5ebe911933 [wpimath] Add DifferentialDriveAccelerationLimiter (#4091)
3919250da2 [wpilibj] Remove finalizers (#4158)
b3aee28388 [commands] Allow BooleanSupplier for Trigger operations (#4103)
9d20ab3024 [wpilib] Allow disabling ElevatorSim gravity (#4145)
aaa69f6717 [ci] Remove 32-bit Windows builds (#4078)
355a11a414 Update Java linters and fix new PMD errors (#4157)
ffc69d406c [examples] Reduce suggested acceleration in Ramsete example (#4171)
922d50079a [wpimath] Units: fix comment in degreesToRotations (NFC) (#4159)
dd163b62ae [wpimath] Rotation2d: Add factory method that uses rotations (#4166)
bd80e220b9 [ci] Upgrade CMake actions (#4161)
aef4b16d4c [wpimath] Remove unnecessary NOLINT in LinearPlantInversionFeedforward (NFC) (#4155)
975171609e [wpilib] Compressor: Rename enabled to isEnabled (#4147)
5bf46a9093 [wpimath] Add ComputerVisionUtil (#4124)
f27a1f9bfb [commands] Fix JoystickButton.getAsBoolean (#4131)
1b26e2d5da [commands] Add RepeatCommand (#4009)
88222daa3d [hal] Fix misspelling in AnalogInput/Output docs (NFC) (#4153)
81c5b41ce1 [wpilibj] Document MechanismLigament2d angle unit (NFC) (#4142)
9650e6733e [wpiutil] DataLog: Document finish and thread safety (NFC) (#4140)
c8905ec29a [wpimath] Remove ImplicitModelFollower dt argument (#4119)
b4620f01f9 [wpimath] Fix Rotation2d interpolation in Java (#4125)
2e462a19d3 [wpimath] Constexprify units unary operators (#4138)
069f932e59 [build] Fix gl3w cmake build (#4139)
126e3de91a [wpilibc] Remove unused SetPriority() call from Ultrasonic (#4123)
ba0dccaae4 [wpimath] Fix reference to Rotation2d.fromRadians() (#4118)
e1b6e5f212 [wpilib] Improve MotorSafety documentation (NFC) (#4120)
8d79dc8738 [wpimath] Add ImplicitModelFollower (#4056)
78108c2aba [wpimath] Fix PIDController having incorrect error after calling SetSetpoint() (#4070)
cdafc723fb [examples] Remove unused LinearPlantInversionFeedforward includes (#4069)
0d70884dce [wpimath] Add InterpolatedTreeMap (#4073)
765efa325e [wpimath] Remove redundant column index from vectors (#4116)
89ffcbbe41 [wpimath] Update TrapezoidProfile class name in comment (NFC) (#4107)
95ae23b0e7 [wpimath] Improve EKF numerical stability (#4093)
d5cb6fed67 [wpimath] Support zero cost entries in MakeCostMatrix() (#4100)
d0fef18378 [wpimath] Remove redundant `this.` from ExtendedKalmanFilter.java (#4115)
d640c0f41f [wpimath] Fix pose estimator local measurement standard deviation docs (NFC) (#4113)
a2fa5e3ff7 [wpilibc] BatterySim: Provide non-initializer list versions of Calculate (#4076)
a3eea9958e [hal] Add link to FRC CAN Spec (NFC) (#4086)
db27331d7b [wpilib] Update DifferentialDrive docs (NFC) (#4085)
fdfb31f164 [dlt] Export boolean[] values (#4082)
f93c3331b3 [wpigui] disable changing directory when initializing on MacOS (#4092)
ab7ac4fbb9 [build] Fix various warnings in cmake builds (#4081)
bc39a1a293 [wpilibc] Fix moved pneumatics objects not destructing properly (#4068)
2668130e70 [wpimath] Remove SwerveDrivePoseEstimator encoder reset warning (#4066)
d27ed3722b [ci] Set actions workflow concurrency (#4060)
dae18308c9 [wpimath] Minor fixes to Rotation2d docs (NFC) (#4055)
d66555e42f [datalogtool] Add datalogtool
9f52d8a3b1 [wpilib] DriverStation: Add DataLog support for modes and joystick data
757ea91932 [wpilib] Add DataLogManager
02a804f1c5 [ntcore] Add DataLog support
9b500df0d9 [wpiutil] Add high speed data logging
5a89575b3a [wpiutil] Import customized LLVM MemoryBuffer
b8c4d7527b [wpiutil] Add MappedFileRegion
ac5d46cfa7 [wpilibc] Fix ProfiledPID SetTolerance default velocity value (#4054)
bc9e96e86f [wpilib] Absolute Encoder API and behavior fixes (#4052)
f88c435dd0 [hal] Add mechanism to cancel all periodic callbacks (#4049)
Change-Id: I49aa5b08abbefc7a045e99e19d48ce2cd8fc4d1b
git-subtree-dir: third_party/allwpilib
git-subtree-split: 83f1860047c86aa3330fcb41caf3b2047e074804
Signed-off-by: James Kuszmaul <jabukuszmaul+collab@gmail.com>
diff --git a/ntcore/.styleguide b/ntcore/.styleguide
index 8cf3a51..ef9cbb7 100644
--- a/ntcore/.styleguide
+++ b/ntcore/.styleguide
@@ -30,4 +30,5 @@
^fmt/
^support/
^wpi/
+ ^wpinet/
}
diff --git a/ntcore/CMakeLists.txt b/ntcore/CMakeLists.txt
index 44992ef..cd4df28 100644
--- a/ntcore/CMakeLists.txt
+++ b/ntcore/CMakeLists.txt
@@ -3,23 +3,39 @@
include(CompileWarnings)
include(AddTest)
-file(GLOB
- ntcore_native_src src/main/native/cpp/*.cpp
- ntcore_native_src src/main/native/cpp/networktables/*.cpp
- ntcore_native_src src/main/native/cpp/tables/*.cpp)
+execute_process(COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/generate_topics.py ${WPILIB_BINARY_DIR}/ntcore RESULT_VARIABLE generateResult)
+if(NOT (generateResult EQUAL "0"))
+ # Try python
+ execute_process(COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/generate_topics.py ${WPILIB_BINARY_DIR}/ntcore RESULT_VARIABLE generateResult)
+ if(NOT (generateResult EQUAL "0"))
+ message(FATAL_ERROR "python and python3 generate_topics.py failed")
+ endif()
+endif()
+
+file(GLOB ntcore_native_src
+ src/main/native/cpp/*.cpp
+ ${WPILIB_BINARY_DIR}/ntcore/generated/main/native/cpp/*.cpp
+ src/main/native/cpp/net/*.cpp
+ src/main/native/cpp/net3/*.cpp
+ src/main/native/cpp/networktables/*.cpp
+ src/main/native/cpp/tables/*.cpp)
add_library(ntcore ${ntcore_native_src})
set_target_properties(ntcore PROPERTIES DEBUG_POSTFIX "d")
-target_include_directories(ntcore PUBLIC
+target_include_directories(ntcore
+ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/cpp
+ PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src/main/native/include>
+ $<BUILD_INTERFACE:${WPILIB_BINARY_DIR}/ntcore/generated/main/native/include>
$<INSTALL_INTERFACE:${include_dest}/ntcore>)
wpilib_target_warnings(ntcore)
-target_compile_features(ntcore PUBLIC cxx_std_17)
-target_link_libraries(ntcore PUBLIC wpiutil)
+target_compile_features(ntcore PUBLIC cxx_std_20)
+target_link_libraries(ntcore PUBLIC wpinet wpiutil)
set_property(TARGET ntcore PROPERTY FOLDER "libraries")
install(TARGETS ntcore EXPORT ntcore DESTINATION "${main_lib_dest}")
install(DIRECTORY src/main/native/include/ DESTINATION "${include_dest}/ntcore")
+install(DIRECTORY ${WPILIB_BINARY_DIR}/ntcore/generated/main/native/include/ DESTINATION "${include_dest}/ntcore")
if (WITH_FLAT_INSTALL)
set (ntcore_config_dir ${wpilib_dest})
@@ -38,10 +54,11 @@
include(UseJava)
set(CMAKE_JAVA_COMPILE_FLAGS "-encoding" "UTF8" "-Xlint:unchecked")
- file(GLOB
- ntcore_jni_src src/main/native/cpp/jni/NetworkTablesJNI.cpp)
+ file(GLOB ntcore_jni_src
+ src/main/native/cpp/jni/*.cpp
+ ${WPILIB_BINARY_DIR}/ntcore/generated/main/native/cpp/jni/*.cpp)
- file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java)
+ file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java ${WPILIB_BINARY_DIR}/ntcore/generated/*.java)
set(CMAKE_JNI_TARGET true)
if(${CMAKE_VERSION} VERSION_LESS "3.11.0")
@@ -78,6 +95,9 @@
endif()
+add_executable(ntcoredev src/dev/native/cpp/main.cpp)
+target_link_libraries(ntcoredev ntcore)
+
if (WITH_TESTS)
wpilib_add_test(ntcore src/test/native/cpp)
target_include_directories(ntcore_test PRIVATE src/main/native/cpp)
diff --git a/ntcore/build.gradle b/ntcore/build.gradle
index 67674c6..c5464cc 100644
--- a/ntcore/build.gradle
+++ b/ntcore/build.gradle
@@ -1,29 +1,331 @@
+import groovy.json.JsonSlurper;
+import com.hubspot.jinjava.Jinjava;
+import com.hubspot.jinjava.JinjavaConfig;
+
+def ntcoreTypesInputFile = file("src/generate/types.json")
+def ntcoreJavaTypesInputDir = file("src/generate/java")
+def ntcoreJavaTypesOutputDir = file("$buildDir/generated/main/java/edu/wpi/first/networktables")
+
+task ntcoreGenerateJavaTypes() {
+ description = "Generates ntcore Java type classes"
+ group = "WPILib"
+
+ inputs.file ntcoreTypesInputFile
+ inputs.dir ntcoreJavaTypesInputDir
+ outputs.dir ntcoreJavaTypesOutputDir
+
+ doLast {
+ def jsonSlurper = new JsonSlurper()
+ def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
+
+ ntcoreJavaTypesOutputDir.deleteDir()
+ ntcoreJavaTypesOutputDir.mkdirs()
+
+ def config = new JinjavaConfig()
+ def jinjava = new Jinjava(config)
+
+ ntcoreJavaTypesInputDir.listFiles().each { File file ->
+ def template = file.text
+ def outfn = file.name.substring(0, file.name.length() - 6)
+ if (file.name.startsWith("NetworkTable") || file.name.startsWith("Generic")) {
+ def replacements = new HashMap<String,?>()
+ replacements.put("types", jsonTypes)
+ def output = jinjava.render(template, replacements)
+ new File(ntcoreJavaTypesOutputDir, outfn).write(output)
+ } else {
+ jsonTypes.each { Map<String,?> replacements ->
+ def output = jinjava.render(template, replacements)
+ def typename = replacements.get("TypeName")
+ File outfile
+ if (outfn == "Timestamped.java") {
+ outfile = new File(ntcoreJavaTypesOutputDir, "Timestamped${typename}.java")
+ } else {
+ outfile = new File(ntcoreJavaTypesOutputDir, "${typename}${outfn}")
+ }
+ outfile.write(output)
+ }
+ }
+ }
+ }
+}
+
+def ntcoreCppTypesInputDir = file("src/generate/include/networktables")
+def ntcoreCppTypesOutputDir = file("$buildDir/generated/main/native/include/networktables")
+
+task ntcoreGenerateCppTypes() {
+ description = "Generates ntcore C++ type classes"
+ group = "WPILib"
+
+ inputs.file ntcoreTypesInputFile
+ inputs.dir ntcoreCppTypesInputDir
+ outputs.dir ntcoreCppTypesOutputDir
+
+ doLast {
+ def jsonSlurper = new JsonSlurper()
+ def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
+
+ ntcoreCppTypesOutputDir.deleteDir()
+ ntcoreCppTypesOutputDir.mkdirs()
+
+ def config = new JinjavaConfig()
+ def jinjava = new Jinjava(config)
+
+ ntcoreCppTypesInputDir.listFiles().each { File file ->
+ def template = file.text
+ def outfn = file.name.substring(0, file.name.length() - 6)
+ jsonTypes.each { Map<String,?> replacements ->
+ def output = jinjava.render(template, replacements)
+ def typename = replacements.get("TypeName")
+ def outfile = new File(ntcoreCppTypesOutputDir, "${typename}${outfn}")
+ outfile.write(output)
+ }
+ }
+ }
+}
+
+def ntcoreCppHandleSourceInputFile = file("src/generate/cpp/ntcore_cpp_types.cpp.jinja")
+def ntcoreCppHandleSourceOutputFile = file("$buildDir/generated/main/native/cpp/ntcore_cpp_types.cpp")
+
+task ntcoreGenerateCppHandleSource() {
+ description = "Generates ntcore C++ handle source"
+ group = "WPILib"
+
+ inputs.files([
+ ntcoreTypesInputFile,
+ ntcoreCppHandleSourceInputFile
+ ])
+ outputs.file ntcoreCppHandleSourceOutputFile
+
+ doLast {
+ def jsonSlurper = new JsonSlurper()
+ def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
+
+ ntcoreCppHandleSourceOutputFile.delete()
+
+ def config = new JinjavaConfig()
+ def jinjava = new Jinjava(config)
+
+ def template = ntcoreCppHandleSourceInputFile.text
+ def replacements = new HashMap<String,?>()
+ replacements.put("types", jsonTypes)
+ def output = jinjava.render(template, replacements)
+ ntcoreCppHandleSourceOutputFile.write(output)
+ }
+}
+
+def ntcoreCppHandleHeaderInputFile = file("src/generate/include/ntcore_cpp_types.h.jinja")
+def ntcoreCppHandleHeaderOutputFile = file("$buildDir/generated/main/native/include/ntcore_cpp_types.h")
+
+task ntcoreGenerateCppHandleHeader() {
+ description = "Generates ntcore C++ handle header"
+ group = "WPILib"
+
+ inputs.files([
+ ntcoreTypesInputFile,
+ ntcoreCppHandleHeaderInputFile
+ ])
+ outputs.file ntcoreCppHandleHeaderOutputFile
+
+ doLast {
+ def jsonSlurper = new JsonSlurper()
+ def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
+
+ ntcoreCppHandleHeaderOutputFile.delete()
+
+ def config = new JinjavaConfig()
+ def jinjava = new Jinjava(config)
+
+ def template = ntcoreCppHandleHeaderInputFile.text
+ def replacements = new HashMap<String,?>()
+ replacements.put("types", jsonTypes)
+ def output = jinjava.render(template, replacements)
+ ntcoreCppHandleHeaderOutputFile.write(output)
+ }
+}
+
+def ntcoreCHandleSourceInputFile = file("src/generate/cpp/ntcore_c_types.cpp.jinja")
+def ntcoreCHandleSourceOutputFile = file("$buildDir/generated/main/native/cpp/ntcore_c_types.cpp")
+
+task ntcoreGenerateCHandleSource() {
+ description = "Generates ntcore C handle source"
+ group = "WPILib"
+
+ inputs.files([
+ ntcoreTypesInputFile,
+ ntcoreCHandleSourceInputFile
+ ])
+ outputs.file ntcoreCHandleSourceOutputFile
+
+ doLast {
+ def jsonSlurper = new JsonSlurper()
+ def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
+
+ ntcoreCHandleSourceOutputFile.delete()
+
+ def config = new JinjavaConfig()
+ def jinjava = new Jinjava(config)
+
+ def template = ntcoreCHandleSourceInputFile.text
+ def replacements = new HashMap<String,?>()
+ replacements.put("types", jsonTypes)
+ def output = jinjava.render(template, replacements)
+ ntcoreCHandleSourceOutputFile.write(output)
+ }
+}
+
+def ntcoreCHandleHeaderInputFile = file("src/generate/include/ntcore_c_types.h.jinja")
+def ntcoreCHandleHeaderOutputFile = file("$buildDir/generated/main/native/include/ntcore_c_types.h")
+
+task ntcoreGenerateCHandleHeader() {
+ description = "Generates ntcore C handle header"
+ group = "WPILib"
+
+ inputs.files([
+ ntcoreTypesInputFile,
+ ntcoreCHandleHeaderInputFile
+ ])
+ outputs.file ntcoreCHandleHeaderOutputFile
+
+ doLast {
+ def jsonSlurper = new JsonSlurper()
+ def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
+
+ ntcoreCHandleHeaderOutputFile.delete()
+
+ def config = new JinjavaConfig()
+ def jinjava = new Jinjava(config)
+
+ def template = ntcoreCHandleHeaderInputFile.text
+ def replacements = new HashMap<String,?>()
+ replacements.put("types", jsonTypes)
+ def output = jinjava.render(template, replacements)
+ ntcoreCHandleHeaderOutputFile.write(output)
+ }
+}
+
+def ntcoreJniSourceInputFile = file("src/generate/cpp/jni/types_jni.cpp.jinja")
+def ntcoreJniSourceOutputFile = file("$buildDir/generated/main/native/cpp/jni/types_jni.cpp")
+
+task ntcoreGenerateJniSource() {
+ description = "Generates ntcore JNI types source"
+ group = "WPILib"
+
+ inputs.files([
+ ntcoreTypesInputFile,
+ ntcoreJniSourceInputFile
+ ])
+ outputs.file ntcoreJniSourceOutputFile
+
+ doLast {
+ def jsonSlurper = new JsonSlurper()
+ def jsonTypes = jsonSlurper.parse(ntcoreTypesInputFile)
+
+ ntcoreJniSourceOutputFile.delete()
+
+ def config = new JinjavaConfig()
+ def jinjava = new Jinjava(config)
+
+ def template = ntcoreJniSourceInputFile.text
+ def replacements = new HashMap<String,?>()
+ replacements.put("types", jsonTypes)
+ def output = jinjava.render(template, replacements)
+ ntcoreJniSourceOutputFile.write(output)
+ }
+}
+
ext {
+ addNtcoreDependency = { binary, shared->
+ binary.tasks.withType(AbstractNativeSourceCompileTask) {
+ it.dependsOn ntcoreGenerateCppTypes
+ it.dependsOn ntcoreGenerateCppHandleHeader
+ it.dependsOn ntcoreGenerateCHandleHeader
+ }
+ binary.lib project: ':ntcore', library: 'ntcore', linkage: shared
+ }
+
+ addNtcoreJniDependency = { binary->
+ binary.tasks.withType(AbstractNativeSourceCompileTask) {
+ it.dependsOn ntcoreGenerateCppTypes
+ it.dependsOn ntcoreGenerateCppHandleHeader
+ it.dependsOn ntcoreGenerateCHandleHeader
+ }
+ binary.lib project: ':ntcore', library: 'ntcoreJNIShared', linkage: 'shared'
+ }
+
nativeName = 'ntcore'
devMain = 'edu.wpi.first.ntcore.DevMain'
+ generatedSources = "$buildDir/generated/main/native/cpp"
+ generatedHeaders = "$buildDir/generated/main/native/include"
+ jniSplitSetup = {
+ it.tasks.withType(CppCompile) {
+ it.dependsOn ntcoreGenerateCppTypes
+ it.dependsOn ntcoreGenerateCppHandleSource
+ it.dependsOn ntcoreGenerateCppHandleHeader
+ it.dependsOn ntcoreGenerateCHandleSource
+ it.dependsOn ntcoreGenerateCHandleHeader
+ it.dependsOn ntcoreGenerateJniSource
+ }
+ }
+ splitSetup = {
+ it.tasks.withType(CppCompile) {
+ it.dependsOn ntcoreGenerateCppTypes
+ it.dependsOn ntcoreGenerateCppHandleSource
+ it.dependsOn ntcoreGenerateCppHandleHeader
+ it.dependsOn ntcoreGenerateCHandleSource
+ it.dependsOn ntcoreGenerateCHandleHeader
+ it.dependsOn ntcoreGenerateJniSource
+ it.includes 'src/main/native/cpp'
+ }
+ }
+ exeSplitSetup = {
+ it.tasks.withType(CppCompile) {
+ it.dependsOn ntcoreGenerateCppTypes
+ it.dependsOn ntcoreGenerateCppHandleSource
+ it.dependsOn ntcoreGenerateCppHandleHeader
+ it.dependsOn ntcoreGenerateCHandleSource
+ it.dependsOn ntcoreGenerateCHandleHeader
+ }
+ }
}
apply from: "${rootDir}/shared/jni/setupBuild.gradle"
+model {
+ components {}
+ binaries {
+ all {
+ if (!it.buildable || !(it instanceof NativeBinarySpec)) {
+ return
+ }
+ if (it.component.name == "${nativeName}JNI") {
+ lib project: ':wpinet', library: 'wpinet', linkage: 'static'
+ lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
+ } else {
+ lib project: ':wpinet', library: 'wpinet', linkage: 'shared'
+ lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared'
+ }
+ }
+ }
+}
+
+sourceSets.main.java.srcDir "${buildDir}/generated/main/java"
+compileJava.dependsOn ntcoreGenerateJavaTypes
+
+cppHeadersZip {
+ dependsOn ntcoreGenerateCppTypes
+ dependsOn ntcoreGenerateCppHandleHeader
+ dependsOn ntcoreGenerateCHandleHeader
+ from(generatedHeaders) {
+ into '/'
+ }
+}
+
Action<List<String>> symbolFilter = { symbols ->
symbols.removeIf({ !it.startsWith('NT_') })
} as Action<List<String>>;
nativeUtils.exportsConfigs {
ntcore {
- x86ExcludeSymbols = [
- '_CT??_R0?AV_System_error',
- '_CT??_R0?AVexception',
- '_CT??_R0?AVfailure',
- '_CT??_R0?AVruntime_error',
- '_CT??_R0?AVsystem_error',
- '_CTA5?AVfailure',
- '_TI5?AVfailure',
- '_CT??_R0?AVout_of_range',
- '_CTA3?AVout_of_range',
- '_TI3?AVout_of_range',
- '_CT??_R0?AVbad_cast'
- ]
x64ExcludeSymbols = [
'_CT??_R0?AV_System_error',
'_CT??_R0?AVexception',
@@ -39,7 +341,6 @@
]
}
ntcoreJNI {
- x86SymbolFilter = symbolFilter
x64SymbolFilter = symbolFilter
}
}
diff --git a/ntcore/doc/networktables4.adoc b/ntcore/doc/networktables4.adoc
new file mode 100644
index 0000000..5384ebf
--- /dev/null
+++ b/ntcore/doc/networktables4.adoc
@@ -0,0 +1,819 @@
+= Network Tables Protocol Specification, Version 4.0
+WPILib Developers <wpilib@wpi.edu>
+Protocol Revision 4.0, 2/14/2021
+:toc:
+:toc-placement: preamble
+:sectanchors:
+
+A pub/sub WebSockets protocol based on NetworkTables concepts.
+
+[[motivation]]
+== Motivation
+
+Currently in NetworkTables there is no way to synchronize user value updates and NT update sweeps, and if user value updates occur more frequently than NT update sweeps, the intermediate values are lost. This prevents NetworkTables from being a viable transport layer for seeing all value changes (e.g. for plotting) at rates higher than the NetworkTables update rate (e.g. for capturing high frequency PID changes). While custom code can work around the second issue, it is more difficult to work around the first issue (unless full timestamps are also sent).
+
+Adding built-in support for capturing and communicating all timestamped data value changes with minimal additional user code changes will make it much easier for inexperienced teams to get high resolution, accurate data to dashboard displays with the minimal possible bandwidth and airtime usage. Assuming the dashboard performs record and playback of NT updates, this also meets the desire to provide teams a robust data capture and playback mechanism.
+
+Migration to a pub/sub style protocol layer where the client can request only its desired topics and the frequency in which it wants to receive value changes provides some mitigation for increased network utilization due to other changes.
+
+Using WebSockets (RFC 6455) as the transport layer along with JSON and MessagePack data encoding enables browser-based dashboards and other web technologies to easily access the protocol and minimizes the amount of custom wire protocol implementation required. Support for this protocol can easily be added in parallel to the existing NetworkTables protocol stack in order to maintain compatibility with older clients/servers and facilitate a gradual transition.
+
+[[references]]
+== References
+
+[[rfc6455,RFC6455,WebSocket]]
+* RFC 6455, The WebSocket Protocol, https://tools.ietf.org/html/rfc6455
+
+[[rfc7159,RFC7159,JSON]]
+* RFC 7159, The JavaScript Object Notation (JSON) Data Interchange Format, https://tools.ietf.org/html/rfc7159
+
+[[messagepack]]
+* MessagePack, MessagePack Specification, https://github.com/msgpack/msgpack/blob/master/spec.md
+
+[[networktables3]]
+* <<networktables3.adoc#, Network Tables Protocol Specification, Version 3.0>>
+
+[[cristians-algorithm]]
+* Cristian's algorithm, https://en.wikipedia.org/wiki/Cristian%27s_algorithm
+
+[[design]]
+== Design
+
+Both text and binary <<WebSocket,WebSocket>> frames are used for transport. Text frames are <<JSON,JSON>>, and binary frames are <<messagepack,MessagePack>>. JSON is used for control messages for human readability and ease of debugging. MessagePack is used only for time and space efficient encoding of values, and the MessagePack subset used in this protocol is small enough that a custom encoder/decoder can be easily implemented for languages that lack good MessagePack libraries.
+
+Consistent with pub/sub nomenclature, this spec uses the term "topic" for what was called an "entry" or "key" in the <<networktables3,NT 3.0 spec>>.
+
+MessagePack messages use numeric IDs for topics to reduce data size; these IDs have a 1:1 mapping to a full topic name (a string). This mapping is communicated via the JSON <<msg-announce>>.
+
+[[topic-names]]
+=== Topic Names
+
+Topic have UTF-8 string names. Names starting with `$` are reserved for use by the server (see <<meta-topics>>). Otherwise names are unrestricted, but convention is for them to start with `/` and be `/`-separated (without consecutive `//`) to make a Unix-path-like structure.
+
+[[timestamps]]
+=== Timestamps
+
+All MessagePack messages are timestamped. Timestamps are specified in unsigned integer microseconds--for robot programs, microseconds are preferred as the FPGA provides time in microseconds, and integers result in a more compact wire encoding (in typical robot use, integer microseconds timestamps will be half the size of double timestamps until about T=70 minutes, and in the worst case will be no larger).
+
+Timestamps in messages always use the server time base. The server time base is synchronized across the network by the means described in this section (an implementation of <<cristians-algorithm, Cristian's algorithm>>).
+
+Implementations should expose the actual timestamps for messages as well as provide methods to convert from server time base to local time to facilitate applications that prefer the server time base (e.g. a dashboard showing robot events might prefer to use server time instead of wall clock time).
+
+The topic ID of -1 is reserved for timestamp communication. Clients shall periodically (e.g. every few seconds) send, in a manner that minimizes transmission delays, a MessagePack message with ID of -1, a timestamp of 0, and an implementation-selected type and data value (typically int or float 64) containing its (the client's) current local time.
+
+When the server receives an -1 ID message from a client, it shall respond to the client, in a manner that minimizes transmission delays, with a MessagePack message with ID=-1, a timestamp of its (the server's) current local time (in microseconds), and the client-provided data value.
+
+When the client receives an -1 ID message from the server, it shall compute the round trip time (RTT) from the delta between the message's data value and the current local time. If the RTT is less than that from previous measurements, the client shall use the timestamp in the message plus ½ the RTT as the server time equivalent to the current local time, and use this equivalence to compute server time base timestamps from local time for future messages.
+
+Due to the fact there can be multiple publishers for a single topic and unpredictable network delays / clock drift, there is no global total order for timestamps either globally or on a per-topic basis. While single publishers for real-time data will be the norm, and in that case the timestamps will usually be in order, applications that use timestamps need to be able to handle out-of-order timestamps.
+
+[[reconnection]]
+=== Caching and Reconnection Handling
+
+Servers shall keep a retained value for each topic for the purposes of <<msg-subscribe>> requests; the retained value shall be the value in the largest timestamp (greater-than or equal-to) message received for that topic. This retained value is deleted if the topic is deleted (e.g. there are no more publishers).
+
+Clients may similarly keep a retained value for each topic for ease of use by user code. If this is done, this retained value shall be updated by both locally published values and received messages for that topic with greater-than/equal-to timestamps, and the retained value shall be deleted when a <<msg-unannounce>> is received.
+
+Clients should support a "set default" operation for a topic. This is a "weak" value update that publishes a value with a timestamp of 0 (thereby not causing the retained value of the server or other clients to be updated if they have a current value update with a timestamp > 0). Typically the "set default" operation should also result in the retained property being set on the topic.
+
+Clients may accept application commands to publish and subscribe while disconnected. If a client does so, in addition to maintaining a retained value as described above, it must keep track for each application-published topic whether any of the locally published values were "strong" (via a "set" operation), or all of them were "weak" (via a "set default" operation). While disconnected, there is no reference clock; "strong" timestamps shall be set to 1 and "weak" timestamps shall be set to 0.
+
+When the client disconnects, the client shall delete any topics that are not published by the application and do not have the retained or persistent properties set. The client shall also reset the application-published retained value timestamps to 0 and 1 as per the previous paragraph.
+
+When the connection to the server is established (either reconnect or initial connection), the client shall publish and send _only_ the retained values to the server that are in application-published topics (those with timestamps of 0 and 1, per above). Only the values with timestamp 0 may be sent immediately upon reconnection. The values with timestamp 1 must wait until the client clock is synchronized with the server clock; the timestamps for these values when sent to the server must be either the current server time or, if possible, an estimation of server time when the values were actually written.
+
+Note: the previous paragraphs enable offline, multi-publisher operation under network/server reboot conditions without creating zombie topics, assuming clients use "set default" and the retained property appropriately. This is achieved mainly via the use of timestamps 0 and 1 to enable tie breaks such that normally-set values (timestamp X) are used in preference to retained values (timestamp 1), and retained values are used in preference to weakly set values (timestamp 0). An example use case is as follows:
+
+* Server starts
+* Dashboard client connects
+* Coprocessor client connects
+* Coprocessor client publishes configuration topic, sends an initial value using "set default", and subscribes to the topic (to detect configuration changes)
+* Dashboard client sees configuration topic published and subscribes to it
+* Dashboard user changes configuration value--dashboard client publishes to the topic and sends the user value
+* Coprocessor receives the user value and updates its retained value
+* **Server reboots** (this also disconnects the dashboard and coprocessor clients)
+* If the dashboard reconnects first:
+** The user value was published and cached (retained value) on the dashboard client, so the dashboard client re-publishes and sends the cached data with timestamp 1.
+** The coprocessor client reconnects later. It also published and cached, but it only ever called "set default" and sends the cached data (which is also the user value) with timestamp 0. It receives the retained value from the server with timestamp 1, and updates locally.
+** The server propagates the timestamp 0 message, but since it has a retained value with timestamp 1, as do other clients, the retained value is not updated and the user value remains active.
+* If the coprocessor reconnects first:
+** The coprocessor client only ever called "set default", so it sends the cached data (the user-set value) with timestamp 0.
+** If the dashboard never reconnects, no new values are published, so the user-set value is active
+** If the dashboard reconnects, it sends a message with timestamp 1 ("strong" set). This propagates but does not change the value (it's the same user-set as before).
+* If the dashboard updates the value while offline, it's still a "strong set" and wins the tie
+
+A second use case:
+
+* Server starts
+* Dashboard client connects
+* Dashboard client publishes configuration value with the retained property set, and publishes an initial value
+* Dashboard client disconnects. The topic is *not* deleted on the server because the retained property is set.
+* Coprocessor client connects
+* Server sends announce message for topic and sends retained value (with timestamp 1)
+* Coprocessor client publishes, uses "set default", and subscribes to the topic. Since "set default" uses a timestamp of 0, it loses to the retained value with timestamp 1, and the coprocessor subscriber will see the value previously set by the dashboard.
+* Dashboard client reconnects
+
+[[server]]
+=== Server Behavior
+
+Topic IDs may be common across all client connections or be connection-specific. If they are common, the server needs to be careful regarding topic ID reuse due to deleted topics, as the protocol provides no way to change a client topic ID. Requests (e.g. <<msg-subscribe,`subscribe`>> or <<msg-publish,`publish`>>) are always specific to a single client connection.
+
+The server shall keep a publisher count for each topic. Persistent and retained topics have an additional implicit publisher. When the publisher count reaches zero (which only happens for non-persistent and non-retained topics), the server shall delete the topic (including its retained value). When a client connection is lost, the server shall handle that as an implicit <<msg-unpublish,`unpublish`>> for all topics currently published by that client.
+
+The server may operate in-process to an application (e.g. a robot program). In this case, the application operationally behaves like a client (e.g. it sends publish requests and receives topic announcements), but of course does not need to estimate delta time, create JSON/MessagePack messages, etc, as all of the necessary operations can be performed programmatically within the same process.
+
+[[client]]
+=== Client Behavior
+
+Clients are responsible for keeping server connections established (e.g. via retries when a connection is lost). Topic IDs must be treated as connection-specific; if the connection to the server is lost, the client is responsible for sending new <<msg-publish,`publish`>> and <<msg-subscribe,`subscribe`>> messages as required for the application when a new connection is established, and not using old topic IDs, but rather waiting for new <<msg-announce,`announce`>> messages to be received.
+
+Except for offline-published values with timestamps of 0, the client shall not send any other published values to the server until its clock is synchronized with the server per the <<timestamps>> section.
+
+Clients may publish a value at any time following clock synchronization. Clients may subscribe to meta-topics to determine whether or not to publish a value change (e.g. based on whether there are any subscribers, or based on specific <<sub-options>>).
+
+[[meta-topics]]
+=== Server-Published Meta Topics
+
+The server shall publish a standard set of topics with information about server state. Clients may subscribe to these topics for diagnostics purposes or to determine when to publish value changes. These topics are hidden--they are not announced to subscribers to an empty prefix, only to subscribers that have subscribed to `"$"` or longer prefixes.
+
+[cols="1,1,2", options="header"]
+|===
+|Topic Name|Data Type|Description
+|<<meta-clients,`$clients`>>|`msgpack`|Connected clients
+|<<meta-client-sub,`$clientsub$<client>`>>|`msgpack`|Client `<client>` subscriptions
+|<<meta-server-sub,`$serversub`>>|`msgpack`|Server subscriptions
+|<<meta-sub,`$sub$<topic>`>>|`msgpack`|Subscriptions to `<topic>`
+|<<meta-client-pub,`$clientpub$<client>`>>|`msgpack`|Client `<client>` publishers
+|<<meta-server-pub,`$serverpub`>>|`msgpack`|Server publishers
+|<<meta-pub,`$pub$<topic>`>>|`msgpack`|Publishers to `<topic>`
+|===
+
+[[meta-clients]]
+==== Connected Clients (`$clients`)
+
+The server shall update this topic when a client connects or disconnects.
+
+The MessagePack contents shall be an array of maps. Each map in the array shall have the following contents:
+
+[cols="1,1,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`id`
+|String
+|Client name
+|
+
+|`conn`
+|String
+|Connection info
+|Connection information about the client; typically host:port
+|===
+
+[[meta-client-sub]]
+==== Client Subscriptions (`$clientsub$<client>`)
+
+The server shall update this topic when the corresponding client subscribes or unsubscribes to any topic.
+
+The MessagePack contents shall be an array of maps. Each map in the array shall have the following contents:
+
+[cols="1,2,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`uid`
+|Integer
+|Subscription UID
+|A client-generated unique identifier for this subscription.
+
+|`topics`
+|Array of String
+|Array of topic names or prefixes
+|One or more topic names or prefixes (if the `prefix` option is true) that messages are sent for.
+
+|`options`
+|Map
+|Options
+|<<sub-options>>
+|===
+
+[[meta-server-sub]]
+==== Server Subscriptions (`$serversub`)
+
+Same as `$clientsub`, except it's updated when the server subscribes or unsubscribes to any topic.
+
+[[meta-sub]]
+==== Subscriptions (`$sub$<topic>`)
+
+The server shall update this topic when a client subscribes or unsubscribes to `<topic>`.
+
+The MessagePack contents shall be an array of maps. Each map in the array shall have the following contents:
+
+[cols="1,2,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`client`
+|String
+|Client name
+|Empty string for server subscriptions.
+
+|`subuid`
+|Integer
+|Subscription UID
+|A client-generated unique identifier for this subscription.
+
+|`options`
+|Map
+|Options
+|<<sub-options>>
+|===
+
+[[meta-client-pub]]
+==== Client Publishers (`$clientpub$<client>`)
+
+The server shall update this topic when the corresponding client publishes or unpublishes any topic.
+
+The MessagePack contents shall be an array of maps. Each map in the array shall have the following contents:
+
+[cols="1,2,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`uid`
+|Integer
+|Publisher UID
+|A client-generated unique identifier for this publisher.
+
+|`topic`
+|String
+|Topic name
+|
+|===
+
+[[meta-server-pub]]
+==== Server Publishers (`$serverpub`)
+
+Same as `$clientpub`, except it's updated when the server publishes or unpublishes any topic.
+
+[[meta-pub]]
+==== Publishers (`$pub$<topic>`)
+
+The server shall update this topic when a client publishes or unpublishes to `<topic>`.
+
+The MessagePack contents shall be an array of maps. Each map in the array shall have the following contents:
+
+[cols="1,2,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`client`
+|String
+|Client name
+|Empty string for server publishers.
+
+|`pubuid`
+|Integer
+|Publisher UID
+|A client-generated unique identifier for this publisher.
+|===
+
+[[websockets-config]]
+== WebSockets Protocol Configuration
+
+Both clients and servers shall support unsecure connections (`ws:`) and may support secure connections (`wss:`). In a trusted network environment (e.g. a robot network), clients that support secure connections should fall back to an unsecure connection if a secure connection is not available.
+
+Servers shall support a resource name of `/nt/<name>`, where `<name>` is an arbitrary string representing the client name. The client name does not need to be unique; multiple connections to the same name are allowed; the server shall ensure the name is unique (for the purposes of meta-topics) by appending a '@' and a unique number (if necessary). To support this, the name provided by the client should not contain an embedded '@'. Clients should provide a way to specify the resource name (in particular, the client name portion).
+
+Both clients and servers shall support/use subprotocol `networktables.first.wpi.edu` for this protocol. Clients and servers shall terminate the connection in accordance with the WebSocket protocol unless both sides support this subprotocol.
+
+The unsecure standard server port number shall be 5810, the secure standard port number shall be 5811.
+
+[[data-types]]
+== Supported Data Types
+
+The following data types are supported. Note: implementations may map integer and float to double internally. Any data type string not in the table below shall be handled in the binary protocol as data type 5 (binary); some specific binary examples are included in the table below.
+
+[cols="1,1,1,1,4",options="header"]
+|===
+|Data type|MessagePack format family|NT 3 data type|Data Type string
+|Notes
+
+|0|bool|Boolean|`boolean`
+|
+
+|1|float 64|Number (double)|`double`
+|
+
+|2|int|Number (double)|`int`
+.2+|Current NetworkTables protocol and user APIs only support double-precision float numeric values; implementations may choose to upgrade APIs to support integer and/or single-precision float values.
+
+|3|float 32|Number (double)|`float`
+
+.2+|4
+.2+|str
+.2+|String
+|`string`
+|
+
+|`json`
+|JSON data (e.g. structured data)
+
+.4+|5
+.4+|bin
+.4+|Raw
+|`raw`
+|Raw data, no specified format
+
+|`rpc`
+|For backwards compatibility with NT 3.0
+
+|`msgpack`
+|Nested MessagePack data (e.g. structured data)
+
+|`protobuf`
+|Google Protocol Buffers data (structured). Uses property `protobuf` to communicate the data description.
+
+|16|array of all bool|Boolean Array|`boolean[]`
+|All elements of the array must be boolean
+
+|17|array of all float 64|Number Array|`double[]`
+|All elements of the array must be double-precision floats
+
+|18|array of all int|Number Array|`int[]`
+|All elements of the array must be integers. See note on Number
+
+|19|array of all float 32|Number Array|`float[]`
+|All elements of the array must be single-precision floats. See note on Number
+
+|20|array of all str|String Array|`string[]`
+|All elements of the array must be text strings
+|===
+
+[[properties]]
+== Properties
+
+Each published topic may also have properties associated to it. Properties are represented in the protocol as JSON and thus property values may be any JSON type. Property keys must be strings. The following properties have a defined meaning in this spec. Servers shall support arbitrary properties being set outside of this set. Clients shall ignore properties they do not recognize. Properties are initially set on publish and may be changed (by any client) using <<msg-setproperties>>.
+
+[cols="1,1,1,6",options="header"]
+|===
+|Property|Type|Description|Notes
+|`persistent`|boolean|Persistent Flag|If true, the last set value will be periodically saved to persistent storage on the server and be restored during server startup. Topics with this property set to true will not be deleted by the server when the last publisher stops publishing.
+|`retained`|boolean|Retained Flag|Topics with this property set to true will not be deleted by the server when the last publisher stops publishing.
+|===
+
+[[sub-options]]
+== Subscription Options
+
+Each subscription may have options set. The following options have a defined meaning in this spec. Servers shall preserve arbitrary options, as servers and clients may support arbitrary options outside of this set. Options are set using <<msg-subscribe>> and cannot be changed.
+
+[cols="1,1,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`periodic` (optional)
+|Number
+|Periodic sweep time (in seconds)
+|How frequently the server should send changes. The server may send more frequently than this (e.g. use a combined minimum period for all values) or apply a restricted range to this value. The default if unspecified is 100 ms (same as NT 3.0).
+
+|`all` (optional)
+|Boolean
+|All Changes Flag
+|If true, the server should send all value changes over the wire. If false, only the most recent value is sent (same as NT 3.0 behavior). If not specified, defaults to false.
+
+|`topicsonly` (optional)
+|Boolean
+|No Value Changes Flag
+|If true, the server should not send any value changes over the wire regardless of other options. This is useful for only getting topic announcements. If false, value changes are sent in accordance with other options. If not specified, defaults to false.
+
+|`prefix` (optional)
+|Boolean
+|Prefix Flag
+|If true, any topic starting with the name in the subscription `topics` list is subscribed to, not just exact matches. If not specified, defaults to false.
+|===
+
+[[text-frames]]
+== Text Data Frames
+
+Each WebSockets text data frame shall consist of a list of <<JSON,JSON>> objects ("JSON messages").
+
+Each JSON message shall be a JSON object with two keys: a `method` key containing a lowercase string value describing the type of message as per the following table, and a `params` key containing the message parameters as a JSON object. The contents of the params object depends on the method; see the sections for each message for details.
+
+Clients and servers shall ignore JSON messages that:
+
+* are not objects
+* have no `method` key or `params` key
+* have a `method` value that is not a string
+* have a `params` value that is not an object
+* have a `method` value that is not listed in the below table
+
+[cols="1,2,2,3",options="header"]
+|===
+|Method
+|Description
+|Direction
+|Response
+
+4+|Publish Messages (Client to Server)
+
+|<<msg-publish,`publish`>>
+|Publish Request
+|Client to Server
+|<<msg-announce,`announce`>>
+
+|<<msg-unpublish,`unpublish`>>
+|Publish Release
+|Client to Server
+|<<msg-unannounce,`unannounce`>> (if topic deleted)
+
+|<<msg-setproperties,`setproperties`>>
+|Set Properties
+|Client to Server
+|<<msg-properties,`properties`>>
+
+4+|Value/Subscription Messages (Client to Server)
+
+|<<msg-subscribe,`subscribe`>>
+|Subscribe
+|Client to Server
+|<<binary-frames,MessagePack messages>> (once topic is announced)
+
+|<<msg-unsubscribe,`unsubscribe`>>
+|Unsubscribe
+|Client to Server
+|---
+
+4+|Announcement Messages (Server to Client)
+
+|<<msg-announce,`announce`>>
+|Topic Announcement
+|Server to Client
+|---
+
+|<<msg-unannounce,`unannounce`>>
+|Topic Removed
+|Server to Client
+|---
+
+|<<msg-properties,`properties`>>
+|Properties Update
+|Server to Client
+|---
+|===
+
+[[publish-messages]]
+=== Publish Messages (Client to Server)
+
+[[msg-publish]]
+==== Publish Request Message (`publish`)
+
+Sent from a client to the server to indicate the client wants to start publishing values at the given topic. The server shall respond with a <<msg-announce>>, even if the topic was previously announced. The client can start publishing data values via MessagePack messages immediately after sending this message, but the messages will be ignored by the server if the publisher data type does not match the topic data type.
+
+The `publish` JSON message shall contain the following parameters:
+
+[cols="1,1,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`name`
+|String
+|Publish name
+|The topic name being published
+
+|`pubuid`
+|Integer
+|Publisher UID
+|A client-generated unique identifier for this publisher. Use the same UID later to unpublish. This is also the identifier that the client will use in MessagePack messages for this topic.
+
+|`type`
+|String
+|Type of data
+|The requested data type (as a string).
+
+If the topic is newly created (e.g. there are no other publishers) this sets the value type. If the topic was previously published, this is ignored. The <<msg-announce,`announce`>> message contains the actual topic value type that the client shall use when publishing values.
+
+Implementations should indicate an error if the user tries to publish an incompatible type to that already set for the topic.
+
+|`properties`
+|Map
+|Properties
+|Initial topic properties.
+
+If the topic is newly created (e.g. there are no other publishers) this sets the topic properties. If the topic was previously published, this is ignored. The <<msg-announce,`announce`>> message contains the actual topic properties. Clients can use the <<msg-setproperties,`setproperties`>> message to change properties after topic creation.
+|===
+
+[[msg-unpublish]]
+==== Publish Release Message (`unpublish`)
+
+Sent from a client to the server to indicate the client wants to stop publishing values for the given topic and publisher. The client should stop publishing data value updates via binary MessagePack messages for this publisher prior to sending this message.
+
+When there are no remaining publishers for a non-persistent topic, the server shall delete the topic and send a <<msg-unannounce>> to all clients who have been sent a previous <<msg-announce>> for the topic.
+
+The `unpublish` JSON message shall contain the following parameters:
+
+[cols="1,1,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`pubuid`
+|Integer
+|Publisher UID
+|The same unique identifier passed to the <<msg-publish,`publish`>> message
+|===
+
+[[msg-setproperties]]
+==== Set Properties Message (`setproperties`)
+
+Sent from a client to the server to change properties (see <<properties>>) for a given topic. The server will send a corresponding <<msg-properties>> to all subscribers to the topic (if the topic is published). This message shall be ignored by the server if the topic is not published.
+
+The `setproperties` JSON message shall contain the following parameters:
+
+[cols="1,2,4",options="header"]
+|===
+|Key
+|Value type
+|Description
+
+|`name`
+|String
+|Topic name
+
+|`update`
+|Map
+|Properties to update
+|===
+
+If a property is not included in the update map, its value is not changed. If a property is provided in the update map with a value of null, the property is deleted.
+
+[[subscription-messages]]
+=== Value/Subscription Messages (Client to Server)
+
+[[msg-subscribe]]
+==== Subscribe Message (`subscribe`)
+
+Sent from a client to the server to indicate the client wants to subscribe to value changes for the specified topics / groups of topics. The server shall send MessagePack messages containing the current values for any existing topics upon receipt, and continue sending MessagePack messages for future value changes. If a topic does not yet exist, no message is sent until it is created (via a publish), at which point a <<msg-announce>> will be sent and MessagePack messages will automatically follow as they are published.
+
+Subscriptions may overlap; only one MessagePack message is sent per value change regardless of the number of subscriptions. Sending a `subscribe` message with the same subscription UID as a previous `subscribe` message results in updating the subscription (replacing the array of identifiers and updating any specified options).
+
+The `subscribe` JSON message shall contain the following parameters:
+
+[cols="1,2,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`topics`
+|Array of String
+|Array of topic names or prefixes
+|One or more topic names or prefixes (if the `prefix` option is true) to start receiving messages for.
+
+|`subuid`
+|Integer
+|Subscription UID
+|A client-generated unique identifier for this subscription. Use the same UID later to unsubscribe.
+
+|`options`
+|Map
+|Options
+|<<sub-options>>
+|===
+
+[[msg-unsubscribe]]
+==== Unsubscribe Message (`unsubscribe`)
+
+Sent from a client to the server to indicate the client wants to stop subscribing to messages for the given subscription.
+
+The `unsubscribe` JSON message shall contain the following parameters:
+
+[cols="1,1,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`subuid`
+|Integer
+|Subscription UID
+|The same unique identifier passed to the <<msg-subscribe,`subscribe`>> message
+|===
+
+[[announcement-messages]]
+=== Announcement Messages (Server to Client)
+
+[[msg-announce]]
+==== Topic Announcement Message (`announce`)
+
+The server shall send this message for each of the following conditions:
+
+- To all clients subscribed to a matching prefix when a topic is created
+
+- To a client in response to an <<msg-publish>> from that client
+
+The `announce` JSON message shall contain the following parameters:
+
+[cols="1,2,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`name`
+|String
+|Topic name
+|
+
+|`id`
+|Integer
+|Topic ID
+|The identifier that the server will use in MessagePack messages for this topic
+
+|`type`
+|String
+|Data type
+|The data type for the topic (as a string)
+
+|`pubuid` (optional)
+|Integer
+|Publisher UID
+|If this message was sent in response to a <<msg-publish,`publish`>> message, the Publisher UID provided in that message. Otherwise absent.
+
+|`properties`
+|Map
+|Properties
+|Topic <<properties>>
+|===
+
+[[msg-unannounce]]
+==== Topic Removed Message (`unannounce`)
+
+The server shall send this message when a previously announced (via a <<msg-announce>>) topic is deleted.
+
+The `unannounce` JSON message shall contain the following parameters:
+
+[cols="1,1,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`name`
+|String
+|Topic name
+|
+
+|`id`
+|Integer
+|Topic ID
+|The identifier that the server was using for value updates
+|===
+
+[[msg-properties]]
+==== Properties Update Message (`properties`)
+
+The server shall send this message when a previously announced (via a <<msg-announce>>) topic has its properties changed (via <<msg-setproperties>>).
+
+The `properties` JSON message shall contain the following parameters:
+
+[cols="1,1,2,6",options="header"]
+|===
+|Key
+|Value type
+|Description
+|Notes
+
+|`name`
+|String
+|Topic name
+|
+
+|`ack`
+|Boolean
+|Acknowledgement
+|True if this message is in response to a <<msg-setproperties,`setproperties`>> message from the same client. Otherwise absent.
+
+|`update`
+|Map
+|Properties to update (from <<msg-setproperties,`setproperties`>>)
+|===
+
+The client shall handle the `update` value as follows. If a property is not included in the update map, its value is not changed. If a property is provided in the update map with a value of null, the property is deleted.
+
+[[binary-frames]]
+== Binary Data Frames
+
+Each WebSockets binary data frame shall consist of a <<messagepack,MessagePack>> data stream with one or more complete MessagePack arrays ("MessagePack messages"). MessagePack messages shall not span across WebSockets data frames. It is up to implementations to decide how many MessagePack messages to put into each transmitted WebSockets data frame (as there is an efficiency/latency tradeoff).
+
+Each MessagePack message shall be a MessagePack array with 4 elements. Implementations can either ignore other types of messages (e.g. non-arrays, other numbers of elements) or terminate the connection (allowing this enables use of simplified decoder implementations).
+
+Messages shall consist of (in this order):
+
+* Topic/Publisher ID: unsigned integer, or -1 (RTT measurement)
+* Timestamp: integer microseconds
+* Data type: unsigned integer
+* Data value (see below)
+
+Topic IDs are used for server to client messages. Topic IDs shall be assigned via JSON <<msg-announce,`announce`>> messages. Client implementations shall ignore messages with topic IDs they do not recognize. Server implementations shall not send messages with topic IDs that were not assigned previously with a JSON message.
+
+Publisher IDs are used for client to server messages. Publisher IDs shall be assigned by the client and be communicated to the server via JSON <<msg-publish,`publish`>> messages. Server implementations shall ignore messages with publisher IDs they do not recognize. Client implementations shall not send messages with publisher IDs that were not assigned previously with a JSON message.
+
+Implementations must ignore messages with data values they cannot decode (either by ignoring the message or by terminating the connection), and shall send messages with data values consistent with the above table.
+
+An example double value update would be 17 bytes:
+
+`94` (array with 4 elements)
+
+`32` (topic/publisher ID=50)
+
+`D2 07 27 0E 00` (timestamp of exactly 2 minutes in integer microseconds)
+
+`01` (data type: double-precision float)
+
+`CB 3F BF 97 24 74 53 8E F3` (double value of 0.1234)
+
+For comparison, a double value update in NT 3.0 is 14 bytes (and does not contain a timestamp).
+
+[[drawbacks]]
+== Drawbacks
+
+[[drawback-api]]
+=== API Changes
+
+While the server (robot) APIs can have minimal to no changes, the current NetworkTables API doesn’t directly map to a pub/sub approach, except for the listener API. A new API will be required to take full advantage of the features of this protocol. One big advantage of the current APIs is that the client and server APIs are the same, so if we update the client API it should work on the server as well.
+
+[[drawback-tcp]]
+=== TCP Only
+
+Everything is sent via the WebSockets pipe, which can result in latency spikes due to TCP retransmissions, even for timestamp updates. Should there be a send-via-UDP option? Web technologies generally can’t use UDP but this feature could be useful for other use cases. However, adding this would add significant complexity and might be better left to MQTT or other full-stack alternatives.
+
+[[drawback-client-server]]
+=== No peer-to-peer client connections
+
+This protocol continues the previous NT approach of having all traffic go through the central NT server, rather than supporting direct peer to peer connections. This adds latency but simplifies the overall protocol design and makes it possible to have clients that can’t set up servers (e.g. web browsers).
+
+[[alternatives]]
+== Alternatives
+
+[[alt-do-nothing]]
+=== Do nothing
+
+The major features in this proposal (accessibility to web technologies and timestamping and sending all changes) would not be made available to users. Users would continue to need to deal with these issues manually or by using third-party workarounds.
+
+[[alt-raw-protocol]]
+=== Update the raw NetworkTables protocol (without using WebSockets)
+
+This does not provide one of the major benefits to moving to a WebSockets protocol, which is easy to use by browsers. While current workarounds like pynetworktables2js exist, a protocol revision which does not address this need feels shortsighted.
+
+[[alt-encapsulation]]
+=== Encapsulate the current NT 3.0 protocol in WebSockets
+
+While this makes the current protocol more easily accessible to web technologies, the current protocol does not have integrated support for timestamping or sending all changes. It also requires substantially more custom decoder implementation work than MessagePack, and does not offer human-readable control messages.
+
+[[alt-mqtt]]
+=== Use MQTT or another existing protocol
+
+MQTT requires running a separate server from the robot program, and the robot program to be a client to it (unlike NT, it has no means of doing value updates within the server itself). MQTT natively does not use WebSockets (it’s a custom wire protocol like the current NetworkTables), although there is a WebSockets variant. MQTT is a significantly more complicated protocol with support for things like full QOS.
+
+[[trades]]
+== Trades
+
+[[trade-json-updates]]
+=== JSON option for value updates (rejected)
+
+This was considered, but rejected for two reasons: encoding overhead and spec/implementation effort. In benchmarking on desktop systems, JSON was 25% the speed of MessagePack when encoding doubles (due to text conversion), and in typical robot use, this overhead would largely land on the robot controller, which also has the fewest resources. In addition, requiring implementation of both JSON and MessagePack encoding nearly doubles the amount of encode/decode implementation effort, particularly as JSON does not have good binary data support and would require Base64 or something similar to encode binary data as a string.
+
+[[trade-timestamp]]
+=== Timestamp format
+
+The spec uses integer microseconds. This seems to be a reasonable enough resolution for FRC use and is common with the FPGA clock resolution.
+
+[[unresolved-questions]]
+== Unresolved Questions
diff --git a/ntcore/generate_topics.py b/ntcore/generate_topics.py
new file mode 100644
index 0000000..ece7df2
--- /dev/null
+++ b/ntcore/generate_topics.py
@@ -0,0 +1,121 @@
+import glob
+import os
+import sys
+from jinja2 import Environment, FileSystemLoader
+import json
+
+
+def Output(outPath, outfn, contents):
+ if not os.path.exists(outPath):
+ os.makedirs(outPath)
+
+ outpathname = f"{outPath}/{outfn}"
+
+ if os.path.exists(outpathname):
+ with open(outpathname, "r") as f:
+ if f.read() == contents:
+ return
+
+ # File either doesn't exist or has different contents
+ with open(outpathname, "w") as f:
+ f.write(contents)
+
+
+def main():
+ dirname, _ = os.path.split(os.path.abspath(__file__))
+ cmake_binary_dir = sys.argv[1]
+
+ with open(f"{dirname}/src/generate/types.json") as f:
+ types = json.load(f)
+
+ # Java files
+ env = Environment(
+ loader=FileSystemLoader(f"{dirname}/src/generate/java"), autoescape=False
+ )
+ rootPath = f"{cmake_binary_dir}/generated/main/java/edu/wpi/first/networktables"
+ for fn in glob.glob(f"{dirname}/src/generate/java/*.jinja"):
+ template = env.get_template(os.path.basename(fn))
+ outfn = os.path.basename(fn)[:-6] # drop ".jinja"
+ if os.path.basename(fn).startswith("NetworkTable") or os.path.basename(
+ fn
+ ).startswith("Generic"):
+ output = template.render(types=types)
+ Output(rootPath, outfn, output)
+ else:
+ for replacements in types:
+ output = template.render(replacements)
+ if outfn == "Timestamped.java":
+ outfn2 = f"Timestamped{replacements['TypeName']}.java"
+ else:
+ outfn2 = f"{replacements['TypeName']}{outfn}"
+ Output(rootPath, outfn2, output)
+
+ # C++ classes
+ env = Environment(
+ loader=FileSystemLoader(f"{dirname}/src/generate/include/networktables"),
+ autoescape=False,
+ )
+ rootPath = f"{cmake_binary_dir}/generated/main/native/include/networktables"
+ for fn in glob.glob(f"{dirname}/src/generate/include/networktables/*.jinja"):
+ template = env.get_template(os.path.basename(fn))
+ outfn = os.path.basename(fn)[:-6] # drop ".jinja"
+ for replacements in types:
+ output = template.render(replacements)
+ outfn2 = f"{replacements['TypeName']}{outfn}"
+ Output(rootPath, outfn2, output)
+
+ # C++ handle API (header)
+ env = Environment(
+ loader=FileSystemLoader(f"{dirname}/src/generate/include"), autoescape=False
+ )
+ template = env.get_template("ntcore_cpp_types.h.jinja")
+ output = template.render(types=types)
+ Output(
+ f"{cmake_binary_dir}/generated/main/native/include",
+ "ntcore_cpp_types.h",
+ output,
+ )
+
+ # C++ handle API (source)
+ env = Environment(
+ loader=FileSystemLoader(f"{dirname}/src/generate/cpp"), autoescape=False
+ )
+ template = env.get_template("ntcore_cpp_types.cpp.jinja")
+ output = template.render(types=types)
+ Output(
+ f"{cmake_binary_dir}/generated/main/native/cpp", "ntcore_cpp_types.cpp", output
+ )
+
+ # C handle API (header)
+ env = Environment(
+ loader=FileSystemLoader(f"{dirname}/src/generate/include"), autoescape=False
+ )
+ template = env.get_template("ntcore_c_types.h.jinja")
+ output = template.render(types=types)
+ Output(
+ f"{cmake_binary_dir}/generated/main/native/include",
+ "ntcore_c_types.h",
+ output,
+ )
+
+ # C handle API (source)
+ env = Environment(
+ loader=FileSystemLoader(f"{dirname}/src/generate/cpp"), autoescape=False
+ )
+ template = env.get_template("ntcore_c_types.cpp.jinja")
+ output = template.render(types=types)
+ Output(
+ f"{cmake_binary_dir}/generated/main/native/cpp", "ntcore_c_types.cpp", output
+ )
+
+ # JNI
+ env = Environment(
+ loader=FileSystemLoader(f"{dirname}/src/generate/cpp/jni"), autoescape=False
+ )
+ template = env.get_template("types_jni.cpp.jinja")
+ output = template.render(types=types)
+ Output(f"{cmake_binary_dir}/generated/main/native/cpp/jni", "types_jni.cpp", output)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ntcore/src/dev/native/cpp/main.cpp b/ntcore/src/dev/native/cpp/main.cpp
index f863018..fac64e7 100644
--- a/ntcore/src/dev/native/cpp/main.cpp
+++ b/ntcore/src/dev/native/cpp/main.cpp
@@ -2,14 +2,107 @@
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
-#include <fmt/core.h>
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <cstdlib>
+#include <numeric>
+#include <string_view>
+#include <thread>
+
+#include <fmt/format.h>
+#include <wpi/Synchronization.h>
#include "ntcore.h"
+#include "ntcore_cpp.h"
-int main() {
+void bench();
+
+int main(int argc, char* argv[]) {
+ if (argc == 2 && std::string_view{argv[1]} == "bench") {
+ bench();
+ return EXIT_SUCCESS;
+ }
auto myValue = nt::GetEntry(nt::GetDefaultInstance(), "MyValue");
nt::SetEntryValue(myValue, nt::Value::MakeString("Hello World"));
- fmt::print("{}\n", nt::GetEntryValue(myValue)->GetString());
+ fmt::print("{}\n", nt::GetEntryValue(myValue).GetString());
+}
+
+void PrintTimes(std::vector<int64_t>& times) {
+ std::sort(times.begin(), times.end());
+ int64_t min = times[0];
+ int64_t max = times[times.size() - 1];
+ double mean =
+ static_cast<double>(std::accumulate(times.begin(), times.end(), 0)) /
+ times.size();
+ double sq_sum =
+ std::inner_product(times.begin(), times.end(), times.begin(), 0);
+ double stdev = std::sqrt(sq_sum / times.size() - mean * mean);
+
+ fmt::print("min: {} max: {}, mean: {}, stdev: {}\n", min, max, mean, stdev);
+ fmt::print("min 10: {}\n", fmt::join(times.begin(), times.begin() + 10, ","));
+ fmt::print("max 10: {}\n", fmt::join(times.end() - 10, times.end(), ","));
+}
+
+// benchmark
+void bench() {
+ // set up instances
+ auto client = nt::CreateInstance();
+ auto server = nt::CreateInstance();
+
+ // connect client and server
+ nt::StartServer(server, "bench.json", "127.0.0.1", 0, 10000);
+ nt::StartClient4(client, "client");
+ nt::SetServer(client, "127.0.0.1", 10000);
+
+ using namespace std::chrono_literals;
+ std::this_thread::sleep_for(1s);
+
+ // add "typical" set of subscribers on client and server
+ nt::SubscribeMultiple(client, {{std::string_view{}}});
+ nt::Subscribe(nt::GetTopic(client, "highrate"), NT_DOUBLE, "double",
+ {.sendAll = true, .keepDuplicates = true});
+ nt::SubscribeMultiple(server, {{std::string_view{}}});
+ auto pub = nt::Publish(nt::GetTopic(server, "highrate"), NT_DOUBLE, "double");
+ nt::SetDouble(pub, 0);
+
+ // warm up
+ for (int i = 1; i <= 10000; ++i) {
+ nt::SetDouble(pub, i * 0.01);
+ if (i % 2000 == 0) {
+ std::this_thread::sleep_for(0.02s);
+ }
+ }
+
+ std::vector<int64_t> flushTimes;
+ flushTimes.reserve(100);
+
+ std::vector<int64_t> times;
+ times.reserve(100001);
+
+ // benchmark
+ auto start = std::chrono::high_resolution_clock::now();
+ int64_t now = nt::Now();
+ for (int i = 1; i <= 100000; ++i) {
+ nt::SetDouble(pub, i * 0.01, now);
+ int64_t prev = now;
+ now = nt::Now();
+ times.emplace_back(now - prev);
+ if (i % 2000 == 0) {
+ nt::Flush(server);
+ flushTimes.emplace_back(nt::Now() - now);
+ std::this_thread::sleep_for(0.02s);
+ now = nt::Now();
+ }
+ }
+ auto stop = std::chrono::high_resolution_clock::now();
+
+ fmt::print("total time: {}us\n",
+ std::chrono::duration_cast<std::chrono::microseconds>(stop - start)
+ .count());
+ PrintTimes(times);
+ fmt::print("-- Flush --\n");
+ PrintTimes(flushTimes);
}
diff --git a/ntcore/src/generate/cpp/jni/types_jni.cpp.jinja b/ntcore/src/generate/cpp/jni/types_jni.cpp.jinja
new file mode 100644
index 0000000..a105278
--- /dev/null
+++ b/ntcore/src/generate/cpp/jni/types_jni.cpp.jinja
@@ -0,0 +1,245 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <jni.h>
+
+#include <wpi/jni_util.h>
+
+#include "edu_wpi_first_networktables_NetworkTablesJNI.h"
+#include "ntcore.h"
+
+using namespace wpi::java;
+
+//
+// Globals and load/unload
+//
+{% for t in types %}
+static JClass timestamped{{ t.TypeName }}Cls;
+{%- endfor %}
+{%- for t in types %}
+{%- if t.jni.ToJavaArray == "MakeJObjectArray" %}
+static JClass {{ t.jni.jtype }}Cls;
+{%- endif %}
+{%- endfor %}
+static JException nullPointerEx;
+
+static const JClassInit classes[] = {
+{%- for t in types %}
+ {"edu/wpi/first/networktables/Timestamped{{ t.TypeName }}", ×tamped{{ t.TypeName }}Cls},
+{%- endfor %}
+{%- for t in types %}
+{%- if t.jni.ToJavaArray == "MakeJObjectArray" %}
+ {"{{ t.jni.jtypestr }}", &{{ t.jni.jtype }}Cls},
+{%- endif %}
+{%- endfor %}
+};
+
+static const JExceptionInit exceptions[] = {
+ {"java/lang/NullPointerException", &nullPointerEx},
+};
+
+namespace nt {
+
+bool JNI_LoadTypes(JNIEnv* env) {
+ // Cache references to classes
+ for (auto& c : classes) {
+ *c.cls = JClass(env, c.name);
+ if (!*c.cls) {
+ return false;
+ }
+ }
+
+ for (auto& c : exceptions) {
+ *c.cls = JException(env, c.name);
+ if (!*c.cls) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void JNI_UnloadTypes(JNIEnv* env) {
+ // Delete global references
+ for (auto& c : classes) {
+ c.cls->free(env);
+ }
+}
+
+} // namespace nt
+
+static std::vector<int> FromJavaBooleanArray(JNIEnv* env, jbooleanArray jarr) {
+ CriticalJBooleanArrayRef ref{env, jarr};
+ if (!ref) {
+ return {};
+ }
+ std::span<const jboolean> elements{ref};
+ size_t len = elements.size();
+ std::vector<int> arr;
+ arr.reserve(len);
+ for (size_t i = 0; i < len; ++i) {
+ arr.push_back(elements[i]);
+ }
+ return arr;
+}
+
+static std::vector<std::string> FromJavaStringArray(JNIEnv* env, jobjectArray jarr) {
+ size_t len = env->GetArrayLength(jarr);
+ std::vector<std::string> arr;
+ arr.reserve(len);
+ for (size_t i = 0; i < len; ++i) {
+ JLocal<jstring> elem{
+ env, static_cast<jstring>(env->GetObjectArrayElement(jarr, i))};
+ if (!elem) {
+ return {};
+ }
+ arr.emplace_back(JStringRef{env, elem}.str());
+ }
+ return arr;
+}
+
+{% for t in types %}
+static jobject MakeJObject(JNIEnv* env, nt::Timestamped{{ t.TypeName }} value) {
+ static jmethodID constructor = env->GetMethodID(
+ timestamped{{ t.TypeName }}Cls, "<init>", "(JJ{{ t.jni.jtypestr }})V");
+{%- if t.jni.JavaObject %}
+ JLocal<{{ t.jni.jtype }}> val{env, {{ t.jni.ToJavaBegin }}value.value{{ t.jni.ToJavaEnd }}};
+ return env->NewObject(timestamped{{ t.TypeName }}Cls, constructor,
+ static_cast<jlong>(value.time),
+ static_cast<jlong>(value.serverTime), val.obj());
+{%- else %}
+ return env->NewObject(timestamped{{ t.TypeName }}Cls, constructor,
+ static_cast<jlong>(value.time),
+ static_cast<jlong>(value.serverTime),
+ {{ t.jni.ToJavaBegin }}value.value{{ t.jni.ToJavaEnd }});
+{%- endif %}
+}
+{% endfor %}
+{%- for t in types %}
+static jobjectArray MakeJObject(JNIEnv* env,
+ std::span<const nt::Timestamped{{ t.TypeName }}> arr) {
+ jobjectArray jarr =
+ env->NewObjectArray(arr.size(), timestamped{{ t.TypeName }}Cls, nullptr);
+ if (!jarr) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < arr.size(); ++i) {
+ JLocal<jobject> elem{env, MakeJObject(env, arr[i])};
+ env->SetObjectArrayElement(jarr, i, elem.obj());
+ }
+ return jarr;
+}
+{% endfor %}
+{%- for t in types %}
+{%- if t.jni.ToJavaArray == "MakeJObjectArray" %}
+static jobjectArray MakeJObjectArray(JNIEnv* env, std::span<const {{ t.cpp.ValueType }}> arr) {
+ jobjectArray jarr =
+ env->NewObjectArray(arr.size(), {{ t.jni.jtype }}Cls, nullptr);
+ if (!jarr) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < arr.size(); ++i) {
+ JLocal<jobject> elem{env, {{ t.jni.ToJavaBegin }}arr[i]{{ t.jni.ToJavaEnd }}};
+ env->SetObjectArrayElement(jarr, i, elem.obj());
+ }
+ return jarr;
+}
+{% endif %}
+{%- endfor %}
+
+extern "C" {
+{% for t in types %}
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getAtomic{{ t.TypeName }}
+ * Signature: (I{{ t.jni.jtypestr }})Ledu/wpi/first/networktables/Timestamped{{ t.TypeName }};
+ */
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getAtomic{{ t.TypeName }}
+ (JNIEnv* env, jclass, jint subentry, {{ t.jni.jtype }} defaultValue)
+{
+ return MakeJObject(env, nt::GetAtomic{{ t.TypeName }}(subentry, {{ t.jni.FromJavaBegin }}defaultValue{{ t.jni.FromJavaEnd }}));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: readQueue{{ t.TypeName }}
+ * Signature: (I)[Ledu/wpi/first/networktables/Timestamped{{ t.TypeName }};
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueue{{ t.TypeName }}
+ (JNIEnv* env, jclass, jint subentry)
+{
+ return MakeJObject(env, nt::ReadQueue{{ t.TypeName }}(subentry));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: readQueueValues{{ t.TypeName }}
+ * Signature: (I)[{{ t.jni.jtypestr }}
+ */
+JNIEXPORT {% if t.jni.JavaObject %}jobject{% else %}{{ t.jni.jtype }}{% endif %}Array JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValues{{ t.TypeName }}
+ (JNIEnv* env, jclass, jint subentry)
+{
+ return {{ t.jni.ToJavaArray }}(env, nt::ReadQueueValues{{ t.TypeName }}(subentry));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: set{{ t.TypeName }}
+ * Signature: (IJ{{ t.jni.jtypestr }})Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_set{{ t.TypeName }}
+ (JNIEnv*{% if t.jni.JavaObject %} env{% endif %}, jclass, jint entry, jlong time, {{ t.jni.jtype }} value)
+{
+{%- if t.jni.JavaObject %}
+ if (!value) {
+ nullPointerEx.Throw(env, "value cannot be null");
+ return false;
+ }
+{%- endif %}
+ return nt::Set{{ t.TypeName }}(entry, {{ t.jni.FromJavaBegin }}value{{ t.jni.FromJavaEnd }}, time);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: get{{ t.TypeName }}
+ * Signature: (I{{ t.jni.jtypestr }}){{ t.jni.jtypestr }}
+ */
+JNIEXPORT {{ t.jni.jtype }} JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_get{{ t.TypeName }}
+ (JNIEnv*{% if t.jni.JavaObject %} env{% endif %}, jclass, jint entry, {{ t.jni.jtype }} defaultValue)
+{
+{%- if t.jni.JavaObject %}
+ auto val = nt::GetEntryValue(entry);
+ if (!val || !val.Is{{ t.TypeName }}()) {
+ return defaultValue;
+ }
+ return {{ t.jni.ToJavaBegin }}val.Get{{ t.TypeName }}(){{ t.jni.ToJavaEnd }};
+{%- else %}
+ return nt::Get{{ t.TypeName }}(entry, defaultValue);
+{%- endif %}
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: setDefault{{ t.TypeName }}
+ * Signature: (IJ{{ t.jni.jtypestr }})Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefault{{ t.TypeName }}
+ (JNIEnv*{% if t.jni.JavaObject %} env{% endif %}, jclass, jint entry, jlong, {{ t.jni.jtype }} defaultValue)
+{
+{%- if t.jni.JavaObject %}
+ if (!defaultValue) {
+ nullPointerEx.Throw(env, "defaultValue cannot be null");
+ return false;
+ }
+{%- endif %}
+ return nt::SetDefault{{ t.TypeName }}(entry, {{ t.jni.FromJavaBegin }}defaultValue{{ t.jni.FromJavaEnd }});
+}
+{% endfor %}
+} // extern "C"
diff --git a/ntcore/src/generate/cpp/ntcore_c_types.cpp.jinja b/ntcore/src/generate/cpp/ntcore_c_types.cpp.jinja
new file mode 100644
index 0000000..e74e5cf
--- /dev/null
+++ b/ntcore/src/generate/cpp/ntcore_c_types.cpp.jinja
@@ -0,0 +1,106 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "ntcore_c_types.h"
+
+#include "Value_internal.h"
+#include "ntcore_cpp.h"
+
+using namespace nt;
+
+template <typename T>
+static inline std::span<const T> ConvertFromC(const T* arr, size_t size) {
+ return {arr, size};
+}
+
+static inline std::string_view ConvertFromC(const char* arr, size_t size) {
+ return {arr, size};
+}
+
+static std::vector<std::string> ConvertFromC(const NT_String* arr, size_t size) {
+ std::vector<std::string> v;
+ v.reserve(size);
+ for (size_t i = 0; i < size; ++i) {
+ v.emplace_back(ConvertFromC(arr[i]));
+ }
+ return v;
+}
+
+{% for t in types %}
+static void ConvertToC(const nt::Timestamped{{ t.TypeName }}& in, NT_Timestamped{{ t.TypeName }}* out) {
+ out->time = in.time;
+ out->serverTime = in.serverTime;
+{%- if t.c.IsArray %}
+ out->value = ConvertToC<{{ t.c.ValueType[:-1] }}>(in.value, &out->len);
+{% else %}
+ out->value = in.value;
+{% endif -%}
+}
+{% endfor %}
+
+extern "C" {
+{% for t in types %}
+NT_Bool NT_Set{{ t.TypeName }}(NT_Handle pubentry, int64_t time, {{ t.c.ParamType }} value{% if t.c.IsArray %}, size_t len{% endif %}) {
+{%- if t.c.IsArray %}
+ return nt::Set{{ t.TypeName }}(pubentry, ConvertFromC(value, len), time);
+{%- else %}
+ return nt::Set{{ t.TypeName }}(pubentry, value, time);
+{%- endif %}
+}
+
+NT_Bool NT_SetDefault{{ t.TypeName }}(NT_Handle pubentry, {{ t.c.ParamType }} defaultValue{% if t.c.IsArray %}, size_t defaultValueLen{% endif %}) {
+{%- if t.c.IsArray %}
+ return nt::SetDefault{{ t.TypeName }}(pubentry, ConvertFromC(defaultValue, defaultValueLen));
+{%- else %}
+ return nt::SetDefault{{ t.TypeName }}(pubentry, defaultValue);
+{%- endif %}
+}
+
+{{ t.c.ValueType }} NT_Get{{ t.TypeName }}(NT_Handle subentry, {{ t.c.ParamType }} defaultValue{% if t.c.IsArray %}, size_t defaultValueLen, size_t* len{% endif %}) {
+{%- if t.c.IsArray %}
+ auto cppValue = nt::Get{{ t.TypeName }}(subentry, ConvertFromC(defaultValue, defaultValueLen));
+ return ConvertToC<{{ t.c.ValueType[:-1] }}>(cppValue, len);
+{%- else %}
+ return nt::Get{{ t.TypeName }}(subentry, defaultValue);
+{%- endif %}
+}
+
+void NT_GetAtomic{{ t.TypeName }}(NT_Handle subentry, {{ t.c.ParamType }} defaultValue{% if t.c.IsArray %}, size_t defaultValueLen{% endif %}, struct NT_Timestamped{{ t.TypeName }}* value) {
+{%- if t.c.IsArray %}
+ auto cppValue = nt::GetAtomic{{ t.TypeName }}(subentry, ConvertFromC(defaultValue, defaultValueLen));
+{%- else %}
+ auto cppValue = nt::GetAtomic{{ t.TypeName }}(subentry, defaultValue);
+{%- endif %}
+ ConvertToC(cppValue, value);
+}
+
+void NT_DisposeTimestamped{{ t.TypeName }}(struct NT_Timestamped{{ t.TypeName }}* value) {
+{%- if t.TypeName == "StringArray" %}
+ NT_FreeStringArray(value->value, value->len);
+{%- elif t.c.IsArray %}
+ std::free(value->value);
+{%- endif %}
+}
+
+struct NT_Timestamped{{ t.TypeName }}* NT_ReadQueue{{ t.TypeName }}(NT_Handle subentry, size_t* len) {
+ auto arr = nt::ReadQueue{{ t.TypeName }}(subentry);
+ return ConvertToC<NT_Timestamped{{ t.TypeName }}>(arr, len);
+}
+
+void NT_FreeQueue{{ t.TypeName }}(struct NT_Timestamped{{ t.TypeName }}* arr, size_t len) {
+ for (size_t i = 0; i < len; ++i) {
+ NT_DisposeTimestamped{{ t.TypeName }}(&arr[i]);
+ }
+ std::free(arr);
+}
+
+{%- if not t.c.IsArray %}
+{{ t.c.ValueType }}* NT_ReadQueueValues{{ t.TypeName }}(NT_Handle subentry, size_t* len) {
+ auto arr = nt::ReadQueueValues{{ t.TypeName }}(subentry);
+ return ConvertToC<{{ t.c.ValueType }}>(arr, len);
+}
+{%- endif %}
+
+{% endfor %}
+} // extern "C"
diff --git a/ntcore/src/generate/cpp/ntcore_cpp_types.cpp.jinja b/ntcore/src/generate/cpp/ntcore_cpp_types.cpp.jinja
new file mode 100644
index 0000000..ba8bca7
--- /dev/null
+++ b/ntcore/src/generate/cpp/ntcore_cpp_types.cpp.jinja
@@ -0,0 +1,76 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "ntcore_cpp_types.h"
+
+#include "Handle.h"
+#include "InstanceImpl.h"
+
+namespace nt {
+{% for t in types %}
+bool Set{{ t.TypeName }}(NT_Handle pubentry, {{ t.cpp.ParamType }} value, int64_t time) {
+ if (auto ii = InstanceImpl::Get(Handle{pubentry}.GetInst())) {
+ return ii->localStorage.SetEntryValue(pubentry,
+ Value::Make{{ t.TypeName }}(value, time == 0 ? Now() : time));
+ } else {
+ return {};
+ }
+}
+
+bool SetDefault{{ t.TypeName }}(NT_Handle pubentry, {{ t.cpp.ParamType }} defaultValue) {
+ if (auto ii = InstanceImpl::Get(Handle{pubentry}.GetInst())) {
+ return ii->localStorage.SetDefaultEntryValue(pubentry,
+ Value::Make{{ t.TypeName }}(defaultValue, 1));
+ } else {
+ return {};
+ }
+}
+
+{{ t.cpp.ValueType }} Get{{ t.TypeName }}(NT_Handle subentry, {{ t.cpp.ParamType }} defaultValue) {
+ return GetAtomic{{ t.TypeName }}(subentry, defaultValue).value;
+}
+
+Timestamped{{ t.TypeName }} GetAtomic{{ t.TypeName }}(NT_Handle subentry, {{ t.cpp.ParamType }} defaultValue) {
+ if (auto ii = InstanceImpl::Get(Handle{subentry}.GetInst())) {
+ return ii->localStorage.GetAtomic{{ t.TypeName }}(subentry, defaultValue);
+ } else {
+ return {};
+ }
+}
+
+std::vector<Timestamped{{ t.TypeName }}> ReadQueue{{ t.TypeName }}(NT_Handle subentry) {
+ if (auto ii = InstanceImpl::Get(Handle{subentry}.GetInst())) {
+ return ii->localStorage.ReadQueue{{ t.TypeName }}(subentry);
+ } else {
+ return {};
+ }
+}
+
+std::vector<{% if t.cpp.ValueType == "bool" %}int{% else %}{{ t.cpp.ValueType }}{% endif %}> ReadQueueValues{{ t.TypeName }}(NT_Handle subentry) {
+ std::vector<{% if t.cpp.ValueType == "bool" %}int{% else %}{{ t.cpp.ValueType }}{% endif %}> rv;
+ auto arr = ReadQueue{{ t.TypeName }}(subentry);
+ rv.reserve(arr.size());
+ for (auto&& elem : arr) {
+ rv.emplace_back(std::move(elem.value));
+ }
+ return rv;
+}
+{% if t.cpp.SmallRetType and t.cpp.SmallElemType %}
+{{ t.cpp.SmallRetType }} Get{{ t.TypeName }}(NT_Handle subentry, wpi::SmallVectorImpl<{{ t.cpp.SmallElemType }}>& buf, {{ t.cpp.ParamType }} defaultValue) {
+ return GetAtomic{{ t.TypeName }}(subentry, buf, defaultValue).value;
+}
+
+Timestamped{{ t.TypeName }}View GetAtomic{{ t.TypeName }}(
+ NT_Handle subentry,
+ wpi::SmallVectorImpl<{{ t.cpp.SmallElemType }}>& buf,
+ {{ t.cpp.ParamType }} defaultValue) {
+ if (auto ii = InstanceImpl::Get(Handle{subentry}.GetInst())) {
+ return ii->localStorage.GetAtomic{{ t.TypeName }}(subentry, buf, defaultValue);
+ } else {
+ return {};
+ }
+}
+{% endif %}
+{% endfor %}
+} // namespace nt
diff --git a/ntcore/src/generate/include/networktables/Topic.h.jinja b/ntcore/src/generate/include/networktables/Topic.h.jinja
new file mode 100644
index 0000000..84e80ec
--- /dev/null
+++ b/ntcore/src/generate/include/networktables/Topic.h.jinja
@@ -0,0 +1,436 @@
+// 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>
+
+{{ cpp.INCLUDES }}
+#include <span>
+#include <string_view>
+#include <vector>
+
+#include "networktables/Topic.h"
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+class json;
+} // namespace wpi
+
+namespace nt {
+
+class {{ TypeName }}Topic;
+
+/**
+ * NetworkTables {{ TypeName }} subscriber.
+ */
+class {{ TypeName }}Subscriber : public Subscriber {
+ public:
+ using TopicType = {{ TypeName }}Topic;
+ using ValueType = {{ cpp.ValueType }};
+ using ParamType = {{ cpp.ParamType }};
+ using TimestampedValueType = Timestamped{{ TypeName }};
+{% if cpp.SmallRetType and cpp.SmallElemType %}
+ using SmallRetType = {{ cpp.SmallRetType }};
+ using SmallElemType = {{ cpp.SmallElemType }};
+ using TimestampedValueViewType = Timestamped{{ TypeName }}View;
+{% endif %}
+
+ {{ TypeName }}Subscriber() = default;
+
+ /**
+ * Construct from a subscriber handle; recommended to use
+ * {{TypeName}}Topic::Subscribe() instead.
+ *
+ * @param handle Native handle
+ * @param defaultValue Default value
+ */
+ {{ TypeName }}Subscriber(NT_Subscriber handle, ParamType defaultValue);
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the stored default value.
+ *
+ * @return value
+ */
+ ValueType Get() const;
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+ ValueType Get(ParamType defaultValue) const;
+{% if cpp.SmallRetType and cpp.SmallElemType %}
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the stored default value.
+ *
+ * @param buf storage for returned value
+ * @return value
+ */
+ SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf) const;
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param buf storage for returned value
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+ SmallRetType Get(wpi::SmallVectorImpl<SmallElemType>& buf, ParamType defaultValue) const;
+{% endif %}
+ /**
+ * Get the last published value along with its timestamp
+ * If no value has been published, returns the stored default value and a
+ * timestamp of 0.
+ *
+ * @return timestamped value
+ */
+ TimestampedValueType GetAtomic() const;
+
+ /**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, 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(ParamType defaultValue) const;
+{% if cpp.SmallRetType and cpp.SmallElemType %}
+ /**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the stored default value and a
+ * timestamp of 0.
+ *
+ * @param buf storage for returned value
+ * @return timestamped value
+ */
+ TimestampedValueViewType GetAtomic(
+ wpi::SmallVectorImpl<SmallElemType>& buf) const;
+
+ /**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param buf storage for returned value
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+ TimestampedValueViewType GetAtomic(
+ wpi::SmallVectorImpl<SmallElemType>& buf,
+ ParamType defaultValue) const;
+{% endif %}
+ /**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @return Array of timestamped values; empty array if no new changes have
+ * been published since the previous call.
+ */
+ std::vector<TimestampedValueType> ReadQueue();
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+
+ private:
+ ValueType m_defaultValue;
+};
+
+/**
+ * NetworkTables {{ TypeName }} publisher.
+ */
+class {{ TypeName }}Publisher : public Publisher {
+ public:
+ using TopicType = {{ TypeName }}Topic;
+ using ValueType = {{ cpp.ValueType }};
+ using ParamType = {{ cpp.ParamType }};
+{% if cpp.SmallRetType and cpp.SmallElemType %}
+ using SmallRetType = {{ cpp.SmallRetType }};
+ using SmallElemType = {{ cpp.SmallElemType }};
+{% endif %}
+ using TimestampedValueType = Timestamped{{ TypeName }};
+
+ {{ TypeName }}Publisher() = default;
+
+ /**
+ * Construct from a publisher handle; recommended to use
+ * {{TypeName}}Topic::Publish() instead.
+ *
+ * @param handle Native handle
+ */
+ explicit {{ TypeName }}Publisher(NT_Publisher handle);
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+ void Set(ParamType value, int64_t time = 0);
+
+ /**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param value value
+ */
+ void SetDefault(ParamType value);
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+};
+
+/**
+ * NetworkTables {{ TypeName }} entry.
+ *
+ * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
+ */
+class {{ TypeName }}Entry final : public {{ TypeName }}Subscriber,
+ public {{ TypeName }}Publisher {
+ public:
+ using SubscriberType = {{ TypeName }}Subscriber;
+ using PublisherType = {{ TypeName }}Publisher;
+ using TopicType = {{ TypeName }}Topic;
+ using ValueType = {{ cpp.ValueType }};
+ using ParamType = {{ cpp.ParamType }};
+{% if cpp.SmallRetType and cpp.SmallElemType %}
+ using SmallRetType = {{ cpp.SmallRetType }};
+ using SmallElemType = {{ cpp.SmallElemType }};
+{% endif %}
+ using TimestampedValueType = Timestamped{{ TypeName }};
+
+ {{ TypeName }}Entry() = default;
+
+ /**
+ * Construct from an entry handle; recommended to use
+ * {{TypeName}}Topic::GetEntry() instead.
+ *
+ * @param handle Native handle
+ * @param defaultValue Default value
+ */
+ {{ TypeName }}Entry(NT_Entry handle, ParamType defaultValue);
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ explicit operator bool() const { return m_subHandle != 0; }
+
+ /**
+ * Gets the native handle for the entry.
+ *
+ * @return Native handle
+ */
+ NT_Entry GetHandle() const { return m_subHandle; }
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+
+ /**
+ * Stops publishing the entry if it's published.
+ */
+ void Unpublish();
+};
+
+/**
+ * NetworkTables {{ TypeName }} topic.
+ */
+class {{ TypeName }}Topic final : public Topic {
+ public:
+ using SubscriberType = {{ TypeName }}Subscriber;
+ using PublisherType = {{ TypeName }}Publisher;
+ using EntryType = {{ TypeName }}Entry;
+ using ValueType = {{ cpp.ValueType }};
+ using ParamType = {{ cpp.ParamType }};
+ using TimestampedValueType = Timestamped{{ TypeName }};
+{%- if TypeString %}
+ /** The default type string for this topic type. */
+ static constexpr std::string_view kTypeString = {{ TypeString }};
+{%- endif %}
+
+ {{ TypeName }}Topic() = default;
+
+ /**
+ * Construct from a topic handle; recommended to use
+ * NetworkTableInstance::Get{{TypeName}}Topic() instead.
+ *
+ * @param handle Native handle
+ */
+ explicit {{ TypeName }}Topic(NT_Topic handle) : Topic{handle} {}
+
+ /**
+ * Construct from a generic topic.
+ *
+ * @param topic Topic
+ */
+ explicit {{ TypeName }}Topic(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.
+ *
+{%- if not TypeString %}
+ * @param typeString type string
+{% endif %}
+ * @param defaultValue default value used when a default is not provided to a
+ * getter function
+ * @param options subscribe options
+ * @return subscriber
+ */
+ [[nodiscard]]
+ SubscriberType Subscribe(
+ {% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+{%- if TypeString %}
+ /**
+ * Create a new subscriber to the topic, with specific type string.
+ *
+ * <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 typeString type string
+ * @param defaultValue default value used when a default is not provided to a
+ * getter function
+ * @param options subscribe options
+ * @return subscriber
+ */
+ [[nodiscard]]
+ SubscriberType SubscribeEx(
+ std::string_view typeString, ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+{% endif %}
+ /**
+ * 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.
+ *
+{%- if not TypeString %}
+ * @param typeString type string
+{% endif %}
+ * @param options publish options
+ * @return publisher
+ */
+ [[nodiscard]]
+ PublisherType Publish({% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * 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 typeString type string
+ * @param properties JSON properties
+ * @param options publish options
+ * @return publisher
+ */
+ [[nodiscard]]
+ PublisherType PublishEx(std::string_view typeString,
+ const wpi::json& properties, const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * 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.
+ *
+{%- if not TypeString %}
+ * @param typeString type string
+{% endif %}
+ * @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({% if not TypeString %}std::string_view typeString, {% endif %}ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+{%- if TypeString %}
+ /**
+ * Create a new entry for the topic, with specific type string.
+ *
+ * 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 typeString type string
+ * @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 GetEntryEx(std::string_view typeString, ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+{% endif %}
+};
+
+} // namespace nt
+
+#include "networktables/{{ TypeName }}Topic.inc"
diff --git a/ntcore/src/generate/include/networktables/Topic.inc.jinja b/ntcore/src/generate/include/networktables/Topic.inc.jinja
new file mode 100644
index 0000000..4e7a167
--- /dev/null
+++ b/ntcore/src/generate/include/networktables/Topic.inc.jinja
@@ -0,0 +1,135 @@
+// 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 "networktables/{{ TypeName }}Topic.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline {{ TypeName }}Subscriber::{{ TypeName }}Subscriber(
+ NT_Subscriber handle, {{ cpp.ParamType }} defaultValue)
+ : Subscriber{handle},
+ m_defaultValue{{ '{' }}{{ cpp.DefaultValueCopy|default('defaultValue') }}} {}
+
+inline {{ cpp.ValueType }} {{ TypeName }}Subscriber::Get() const {
+ return Get(m_defaultValue);
+}
+
+inline {{ cpp.ValueType }} {{ TypeName }}Subscriber::Get(
+ {{ cpp.ParamType }} defaultValue) const {
+ return ::nt::Get{{ TypeName }}(m_subHandle, defaultValue);
+}
+{% if cpp.SmallRetType and cpp.SmallElemType %}
+inline {{ cpp.SmallRetType }} {{ TypeName }}Subscriber::Get(wpi::SmallVectorImpl<{{ cpp.SmallElemType }}>& buf) const {
+ return Get(buf, m_defaultValue);
+}
+
+inline {{ cpp.SmallRetType }} {{ TypeName }}Subscriber::Get(wpi::SmallVectorImpl<{{ cpp.SmallElemType }}>& buf, {{ cpp.ParamType }} defaultValue) const {
+ return nt::Get{{ TypeName }}(m_subHandle, buf, defaultValue);
+}
+{% endif %}
+inline Timestamped{{ TypeName }} {{ TypeName }}Subscriber::GetAtomic() const {
+ return GetAtomic(m_defaultValue);
+}
+
+inline Timestamped{{ TypeName }} {{ TypeName }}Subscriber::GetAtomic(
+ {{ cpp.ParamType }} defaultValue) const {
+ return ::nt::GetAtomic{{ TypeName }}(m_subHandle, defaultValue);
+}
+{% if cpp.SmallRetType and cpp.SmallElemType %}
+inline Timestamped{{ TypeName }}View {{ TypeName }}Subscriber::GetAtomic(wpi::SmallVectorImpl<{{ cpp.SmallElemType }}>& buf) const {
+ return GetAtomic(buf, m_defaultValue);
+}
+
+inline Timestamped{{ TypeName }}View {{ TypeName }}Subscriber::GetAtomic(wpi::SmallVectorImpl<{{ cpp.SmallElemType }}>& buf, {{ cpp.ParamType }} defaultValue) const {
+ return nt::GetAtomic{{ TypeName }}(m_subHandle, buf, defaultValue);
+}
+{% endif %}
+inline std::vector<Timestamped{{ TypeName }}>
+{{ TypeName }}Subscriber::ReadQueue() {
+ return ::nt::ReadQueue{{ TypeName }}(m_subHandle);
+}
+
+inline {{ TypeName }}Topic {{ TypeName }}Subscriber::GetTopic() const {
+ return {{ TypeName }}Topic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline {{ TypeName }}Publisher::{{ TypeName }}Publisher(NT_Publisher handle)
+ : Publisher{handle} {}
+
+inline void {{ TypeName }}Publisher::Set({{ cpp.ParamType }} value,
+ int64_t time) {
+ ::nt::Set{{ TypeName }}(m_pubHandle, value, time);
+}
+
+inline void {{ TypeName }}Publisher::SetDefault({{ cpp.ParamType }} value) {
+ ::nt::SetDefault{{ TypeName }}(m_pubHandle, value);
+}
+
+inline {{ TypeName }}Topic {{ TypeName }}Publisher::GetTopic() const {
+ return {{ TypeName }}Topic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline {{ TypeName }}Entry::{{ TypeName }}Entry(
+ NT_Entry handle, {{ cpp.ParamType }} defaultValue)
+ : {{ TypeName }}Subscriber{handle, defaultValue},
+ {{ TypeName }}Publisher{handle} {}
+
+inline {{ TypeName }}Topic {{ TypeName }}Entry::GetTopic() const {
+ return {{ TypeName }}Topic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void {{ TypeName }}Entry::Unpublish() {
+ ::nt::Unpublish(m_pubHandle);
+}
+
+inline {{ TypeName }}Subscriber {{ TypeName }}Topic::Subscribe(
+ {% if not TypeString %}std::string_view typeString, {% endif %}{{ cpp.ParamType }} defaultValue,
+ const PubSubOptions& options) {
+ return {{ TypeName }}Subscriber{
+ ::nt::Subscribe(m_handle, NT_{{ cpp.TYPE_NAME }}, {{ TypeString|default('typeString') }}, options),
+ defaultValue};
+}
+{%- if TypeString %}
+inline {{ TypeName }}Subscriber {{ TypeName }}Topic::SubscribeEx(
+ std::string_view typeString, {{ cpp.ParamType }} defaultValue,
+ const PubSubOptions& options) {
+ return {{ TypeName }}Subscriber{
+ ::nt::Subscribe(m_handle, NT_{{ cpp.TYPE_NAME }}, typeString, options),
+ defaultValue};
+}
+{% endif %}
+inline {{ TypeName }}Publisher {{ TypeName }}Topic::Publish(
+ {% if not TypeString %}std::string_view typeString, {% endif %}const PubSubOptions& options) {
+ return {{ TypeName }}Publisher{
+ ::nt::Publish(m_handle, NT_{{ cpp.TYPE_NAME }}, {{ TypeString|default('typeString') }}, options)};
+}
+
+inline {{ TypeName }}Publisher {{ TypeName }}Topic::PublishEx(
+ std::string_view typeString,
+ const wpi::json& properties, const PubSubOptions& options) {
+ return {{ TypeName }}Publisher{
+ ::nt::PublishEx(m_handle, NT_{{ cpp.TYPE_NAME }}, typeString, properties, options)};
+}
+
+inline {{ TypeName }}Entry {{ TypeName }}Topic::GetEntry(
+ {% if not TypeString %}std::string_view typeString, {% endif %}{{ cpp.ParamType }} defaultValue,
+ const PubSubOptions& options) {
+ return {{ TypeName }}Entry{
+ ::nt::GetEntry(m_handle, NT_{{ cpp.TYPE_NAME }}, {{ TypeString|default('typeString') }}, options),
+ defaultValue};
+}
+{%- if TypeString %}
+inline {{ TypeName }}Entry {{ TypeName }}Topic::GetEntryEx(
+ std::string_view typeString, {{ cpp.ParamType }} defaultValue,
+ const PubSubOptions& options) {
+ return {{ TypeName }}Entry{
+ ::nt::GetEntry(m_handle, NT_{{ cpp.TYPE_NAME }}, typeString, options),
+ defaultValue};
+}
+{% endif %}
+} // namespace nt
diff --git a/ntcore/src/generate/include/ntcore_c_types.h.jinja b/ntcore/src/generate/include/ntcore_c_types.h.jinja
new file mode 100644
index 0000000..d5b2448
--- /dev/null
+++ b/ntcore/src/generate/include/ntcore_c_types.h.jinja
@@ -0,0 +1,151 @@
+// 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 "ntcore_c.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+{% for t in types %}
+/**
+ * Timestamped {{ t.TypeName }}.
+ * @ingroup ntcore_c_handle_api
+ */
+struct NT_Timestamped{{ t.TypeName }} {
+ /**
+ * Time in local time base.
+ */
+ int64_t time;
+
+ /**
+ * Time in server time base. May be 0 or 1 for locally set values.
+ */
+ int64_t serverTime;
+
+ /**
+ * Value.
+ */
+ {{ t.c.ValueType }} value;
+{%- if t.c.IsArray %}
+ /**
+ * Value length.
+ */
+ size_t len;
+{% endif %}
+};
+
+/**
+ * @defgroup ntcore_{{ t.TypeName }}_cfunc {{ t.TypeName }} Functions
+ * @ingroup ntcore_c_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @param value value to publish
+{%- if t.c.IsArray %}
+ * @param len length of value
+{% endif %}
+ */
+NT_Bool NT_Set{{ t.TypeName }}(NT_Handle pubentry, int64_t time, {{ t.c.ParamType }} value{% if t.c.IsArray %}, size_t len{% endif %});
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+{%- if t.c.IsArray %}
+ * @param defaultValueLen length of default value
+{% endif %}
+ */
+NT_Bool NT_SetDefault{{ t.TypeName }}(NT_Handle pubentry, {{ t.c.ParamType }} defaultValue{% if t.c.IsArray %}, size_t defaultValueLen{% endif %});
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+{%- if t.c.IsArray %}
+ * @param defaultValueLen length of default value
+ * @param len length of returned value (output)
+{% endif %}
+ * @return value
+ */
+{{ t.c.ValueType }} NT_Get{{ t.TypeName }}(NT_Handle subentry, {{ t.c.ParamType }} defaultValue{% if t.c.IsArray %}, size_t defaultValueLen, size_t* len{% endif %});
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+{%- if t.c.IsArray %}
+ * @param defaultValueLen length of default value
+{% endif %}
+ * @param value timestamped value (output)
+ */
+void NT_GetAtomic{{ t.TypeName }}(NT_Handle subentry, {{ t.c.ParamType }} defaultValue{% if t.c.IsArray %}, size_t defaultValueLen{% endif %}, struct NT_Timestamped{{ t.TypeName }}* value);
+
+/**
+ * Disposes a timestamped value (as returned by NT_GetAtomic{{ t.TypeName }}).
+ *
+ * @param value timestamped value
+ */
+void NT_DisposeTimestamped{{ t.TypeName }}(struct NT_Timestamped{{ t.TypeName }}* value);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of timestamped values; NULL if no new changes have
+ * been published since the previous call.
+ */
+struct NT_Timestamped{{ t.TypeName }}* NT_ReadQueue{{ t.TypeName }}(NT_Handle subentry, size_t* len);
+
+/**
+ * Frees a timestamped array of values (as returned by NT_ReadQueue{{ t.TypeName }}).
+ *
+ * @param arr array
+ * @param len length of array
+ */
+void NT_FreeQueue{{ t.TypeName }}(struct NT_Timestamped{{ t.TypeName }}* arr, size_t len);
+
+{%- if not t.c.IsArray %}
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @param len length of returned array (output)
+ * @return Array of values; NULL if no new changes have
+ * been published since the previous call.
+ */
+{{ t.c.ValueType }}* NT_ReadQueueValues{{ t.TypeName }}(NT_Handle subentry, size_t* len);
+{%- endif %}
+
+/** @} */
+{% endfor %}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/ntcore/src/generate/include/ntcore_cpp_types.h.jinja b/ntcore/src/generate/include/ntcore_cpp_types.h.jinja
new file mode 100644
index 0000000..e987186
--- /dev/null
+++ b/ntcore/src/generate/include/ntcore_cpp_types.h.jinja
@@ -0,0 +1,154 @@
+// 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 <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "ntcore_c.h"
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+} // namespace wpi
+
+namespace nt {
+{% for t in types %}
+/**
+ * Timestamped {{ t.TypeName }}.
+ * @ingroup ntcore_cpp_handle_api
+ */
+struct Timestamped{{ t.TypeName }} {
+ Timestamped{{ t.TypeName }}() = default;
+ Timestamped{{ t.TypeName }}(int64_t time, int64_t serverTime, {{ t.cpp.ValueType }} value)
+ : time{time}, serverTime{serverTime}, value{std::move(value)} {}
+
+ /**
+ * Time in local time base.
+ */
+ int64_t time = 0;
+
+ /**
+ * Time in server time base. May be 0 or 1 for locally set values.
+ */
+ int64_t serverTime = 0;
+
+ /**
+ * Value.
+ */
+ {{ t.cpp.ValueType }} value = {};
+};
+{% if t.cpp.SmallRetType %}
+/**
+ * Timestamped {{ t.TypeName }} view (for SmallVector-taking functions).
+ * @ingroup ntcore_cpp_handle_api
+ */
+struct Timestamped{{ t.TypeName }}View {
+ Timestamped{{ t.TypeName }}View() = default;
+ Timestamped{{ t.TypeName }}View(int64_t time, int64_t serverTime, {{ t.cpp.SmallRetType }} value)
+ : time{time}, serverTime{serverTime}, value{std::move(value)} {}
+
+ /**
+ * Time in local time base.
+ */
+ int64_t time = 0;
+
+ /**
+ * Time in server time base. May be 0 or 1 for locally set values.
+ */
+ int64_t serverTime = 0;
+
+ /**
+ * Value.
+ */
+ {{ t.cpp.SmallRetType }} value = {};
+};
+{% endif %}
+/**
+ * @defgroup ntcore_{{ t.TypeName }}_func {{ t.TypeName }} Functions
+ * @ingroup ntcore_cpp_handle_api
+ * @{
+ */
+
+/**
+ * Publish a new value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+bool Set{{ t.TypeName }}(NT_Handle pubentry, {{ t.cpp.ParamType }} value, int64_t time = 0);
+
+/**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param pubentry publisher or entry handle
+ * @param defaultValue default value
+ */
+bool SetDefault{{ t.TypeName }}(NT_Handle pubentry, {{ t.cpp.ParamType }} defaultValue);
+
+/**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+{{ t.cpp.ValueType }} Get{{ t.TypeName }}(NT_Handle subentry, {{ t.cpp.ParamType }} defaultValue);
+
+/**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param subentry subscriber or entry handle
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+Timestamped{{ t.TypeName }} GetAtomic{{ t.TypeName}}(NT_Handle subentry, {{ t.cpp.ParamType }} defaultValue);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of timestamped values; empty array if no new changes have
+ * been published since the previous call.
+ */
+std::vector<Timestamped{{ t.TypeName }}> ReadQueue{{ t.TypeName }}(NT_Handle subentry);
+
+/**
+ * Get an array of all value changes since the last call to ReadQueue.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @param subentry subscriber or entry handle
+ * @return Array of values; empty array if no new changes have
+ * been published since the previous call.
+ */
+std::vector<{% if t.cpp.ValueType == "bool" %}int{% else %}{{ t.cpp.ValueType }}{% endif %}> ReadQueueValues{{ t.TypeName }}(NT_Handle subentry);
+{% if t.cpp.SmallRetType and t.cpp.SmallElemType %}
+{{ t.cpp.SmallRetType }} Get{{ t.TypeName }}(NT_Handle subentry, wpi::SmallVectorImpl<{{ t.cpp.SmallElemType }}>& buf, {{ t.cpp.ParamType }} defaultValue);
+
+Timestamped{{ t.TypeName }}View GetAtomic{{ t.TypeName }}(
+ NT_Handle subentry,
+ wpi::SmallVectorImpl<{{ t.cpp.SmallElemType }}>& buf,
+ {{ t.cpp.ParamType }} defaultValue);
+{% endif %}
+/** @} */
+{% endfor %}
+} // namespace nt
diff --git a/ntcore/src/generate/java/Entry.java.jinja b/ntcore/src/generate/java/Entry.java.jinja
new file mode 100644
index 0000000..cbaa782
--- /dev/null
+++ b/ntcore/src/generate/java/Entry.java.jinja
@@ -0,0 +1,15 @@
+// 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 {{ TypeName }} entry.
+ *
+ * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
+ */
+public interface {{ TypeName }}Entry extends {{ TypeName }}Subscriber, {{ TypeName }}Publisher {
+ /** Stops publishing the entry if it's published. */
+ void unpublish();
+}
diff --git a/ntcore/src/generate/java/EntryImpl.java.jinja b/ntcore/src/generate/java/EntryImpl.java.jinja
new file mode 100644
index 0000000..43b31e4
--- /dev/null
+++ b/ntcore/src/generate/java/EntryImpl.java.jinja
@@ -0,0 +1,75 @@
+// 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 {{ TypeName }} implementation. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+final class {{ TypeName }}EntryImpl extends EntryBase implements {{ TypeName }}Entry {
+ /**
+ * Constructor.
+ *
+ * @param topic Topic
+ * @param handle Native handle
+ * @param defaultValue Default value for get()
+ */
+ {{ TypeName }}EntryImpl({{ TypeName }}Topic topic, int handle, {{ java.ValueType }} defaultValue) {
+ super(handle);
+ m_topic = topic;
+ m_defaultValue = defaultValue;
+ }
+
+ @Override
+ public {{ TypeName }}Topic getTopic() {
+ return m_topic;
+ }
+
+ @Override
+ public {{ java.ValueType }} get() {
+ return NetworkTablesJNI.get{{ TypeName }}(m_handle, m_defaultValue);
+ }
+
+ @Override
+ public {{ java.ValueType }} get({{ java.ValueType }} defaultValue) {
+ return NetworkTablesJNI.get{{TypeName}}(m_handle, defaultValue);
+ }
+
+ @Override
+ public Timestamped{{ TypeName }} getAtomic() {
+ return NetworkTablesJNI.getAtomic{{ TypeName }}(m_handle, m_defaultValue);
+ }
+
+ @Override
+ public Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue) {
+ return NetworkTablesJNI.getAtomic{{ TypeName }}(m_handle, defaultValue);
+ }
+
+ @Override
+ public Timestamped{{ TypeName }}[] readQueue() {
+ return NetworkTablesJNI.readQueue{{ TypeName }}(m_handle);
+ }
+
+ @Override
+ public {{ java.ValueType }}[] readQueueValues() {
+ return NetworkTablesJNI.readQueueValues{{ TypeName }}(m_handle);
+ }
+
+ @Override
+ public void set({{ java.ValueType }} value, long time) {
+ NetworkTablesJNI.set{{ TypeName }}(m_handle, time, value);
+ }
+
+ @Override
+ public void setDefault({{ java.ValueType }} value) {
+ NetworkTablesJNI.setDefault{{ TypeName }}(m_handle, 0, value);
+ }
+
+ @Override
+ public void unpublish() {
+ NetworkTablesJNI.unpublish(m_handle);
+ }
+
+ private final {{ TypeName }}Topic m_topic;
+ private final {{ java.ValueType }} m_defaultValue;
+}
diff --git a/ntcore/src/generate/java/GenericEntryImpl.java.jinja b/ntcore/src/generate/java/GenericEntryImpl.java.jinja
new file mode 100644
index 0000000..e4296d7
--- /dev/null
+++ b/ntcore/src/generate/java/GenericEntryImpl.java.jinja
@@ -0,0 +1,317 @@
+// 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 generic implementation. */
+final class GenericEntryImpl extends EntryBase implements GenericEntry {
+ /**
+ * Constructor.
+ *
+ * @param topic Topic
+ * @param handle Native handle
+ */
+ GenericEntryImpl(Topic topic, int handle) {
+ super(handle);
+ m_topic = topic;
+ }
+
+ @Override
+ public Topic getTopic() {
+ return m_topic;
+ }
+
+ @Override
+ public NetworkTableValue get() {
+ return NetworkTablesJNI.getValue(m_handle);
+ }
+{% for t in types %}
+ /**
+ * Gets the entry's value as a {{ t.java.ValueType }}. If the entry does not exist or is of different type, it
+ * will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ @Override
+ public {{ t.java.ValueType }} get{{ t.TypeName }}({{ t.java.ValueType }} defaultValue) {
+ return NetworkTablesJNI.get{{ t.TypeName }}(m_handle, defaultValue);
+ }
+{% if t.java.WrapValueType %}
+ /**
+ * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+ * it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ @Override
+ public {{ t.java.WrapValueType }} get{{ t.TypeName }}({{ t.java.WrapValueType }} defaultValue) {
+ return NetworkTableValue.fromNative{{ t.TypeName }}(
+ get{{ t.TypeName }}(NetworkTableValue.toNative{{ t.TypeName }}(defaultValue)));
+ }
+{% endif -%}
+{% endfor %}
+ @Override
+ public NetworkTableValue[] readQueue() {
+ return NetworkTablesJNI.readQueueValue(m_handle);
+ }
+
+ @Override
+ public boolean set(NetworkTableValue value) {
+ long time = value.getTime();
+ Object otherValue = value.getValue();
+ switch (value.getType()) {
+ case kBoolean:
+ return NetworkTablesJNI.setBoolean(m_handle, time, (Boolean) otherValue);
+ case kInteger:
+ return NetworkTablesJNI.setInteger(
+ m_handle, time, ((Number) otherValue).longValue());
+ case kFloat:
+ return NetworkTablesJNI.setFloat(
+ m_handle, time, ((Number) otherValue).floatValue());
+ case kDouble:
+ return NetworkTablesJNI.setDouble(
+ m_handle, time, ((Number) otherValue).doubleValue());
+ case kString:
+ return NetworkTablesJNI.setString(m_handle, time, (String) otherValue);
+ case kRaw:
+ return NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue);
+ case kBooleanArray:
+ return NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue);
+ case kIntegerArray:
+ return NetworkTablesJNI.setIntegerArray(m_handle, time, (long[]) otherValue);
+ case kFloatArray:
+ return NetworkTablesJNI.setFloatArray(m_handle, time, (float[]) otherValue);
+ case kDoubleArray:
+ return NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue);
+ case kStringArray:
+ return NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue);
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value that will be assigned
+ * @return False if the table key already exists with a different type
+ * @throws IllegalArgumentException if the value is not a known type
+ */
+ @Override
+ public boolean setValue(Object value, long time) {
+ if (value instanceof NetworkTableValue) {
+ return set((NetworkTableValue) value);
+ } else if (value instanceof Boolean) {
+ return setBoolean((Boolean) value, time);
+ } else if (value instanceof Long) {
+ return setInteger((Long) value, time);
+ } else if (value instanceof Float) {
+ return setFloat((Float) value, time);
+ } else if (value instanceof Number) {
+ return setNumber((Number) value, time);
+ } else if (value instanceof String) {
+ return setString((String) value, time);
+ } else if (value instanceof byte[]) {
+ return setRaw((byte[]) value, time);
+ } else if (value instanceof boolean[]) {
+ return setBooleanArray((boolean[]) value, time);
+ } else if (value instanceof long[]) {
+ return setIntegerArray((long[]) value, time);
+ } else if (value instanceof float[]) {
+ return setFloatArray((float[]) value, time);
+ } else if (value instanceof double[]) {
+ return setDoubleArray((double[]) value, time);
+ } else if (value instanceof Boolean[]) {
+ return setBooleanArray((Boolean[]) value, time);
+ } else if (value instanceof Long[]) {
+ return setIntegerArray((Long[]) value, time);
+ } else if (value instanceof Float[]) {
+ return setFloatArray((Float[]) value, time);
+ } else if (value instanceof Number[]) {
+ return setNumberArray((Number[]) value, time);
+ } else if (value instanceof String[]) {
+ return setStringArray((String[]) value, time);
+ } else {
+ throw new IllegalArgumentException(
+ "Value of type " + value.getClass().getName() + " cannot be put into a table");
+ }
+ }
+{% for t in types %}
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ @Override
+ public boolean set{{ t.TypeName }}({{ t.java.ValueType }} value, long time) {
+ return NetworkTablesJNI.set{{ t.TypeName }}(m_handle, time, value);
+ }
+{% if t.java.WrapValueType %}
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ @Override
+ public boolean set{{ t.TypeName }}({{ t.java.WrapValueType }} value, long time) {
+ return set{{ t.TypeName }}(NetworkTableValue.toNative{{ t.TypeName }}(value), time);
+ }
+{% endif -%}
+{% endfor %}
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setNumber(Number value, long time) {
+ return setDouble(value.doubleValue(), time);
+ }
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setNumberArray(Number[] value, long time) {
+ return setDoubleArray(NetworkTableValue.toNativeDoubleArray(value), time);
+ }
+
+ @Override
+ public boolean setDefault(NetworkTableValue defaultValue) {
+ long time = defaultValue.getTime();
+ Object otherValue = defaultValue.getValue();
+ switch (defaultValue.getType()) {
+ case kBoolean:
+ return NetworkTablesJNI.setDefaultBoolean(m_handle, time, (Boolean) otherValue);
+ case kInteger:
+ return NetworkTablesJNI.setDefaultInteger(
+ m_handle, time, ((Number) otherValue).longValue());
+ case kFloat:
+ return NetworkTablesJNI.setDefaultFloat(
+ m_handle, time, ((Number) otherValue).floatValue());
+ case kDouble:
+ return NetworkTablesJNI.setDefaultDouble(
+ m_handle, time, ((Number) otherValue).doubleValue());
+ case kString:
+ return NetworkTablesJNI.setDefaultString(m_handle, time, (String) otherValue);
+ case kRaw:
+ return NetworkTablesJNI.setDefaultRaw(m_handle, time, (byte[]) otherValue);
+ case kBooleanArray:
+ return NetworkTablesJNI.setDefaultBooleanArray(m_handle, time, (boolean[]) otherValue);
+ case kIntegerArray:
+ return NetworkTablesJNI.setDefaultIntegerArray(m_handle, time, (long[]) otherValue);
+ case kFloatArray:
+ return NetworkTablesJNI.setDefaultFloatArray(m_handle, time, (float[]) otherValue);
+ case kDoubleArray:
+ return NetworkTablesJNI.setDefaultDoubleArray(m_handle, time, (double[]) otherValue);
+ case kStringArray:
+ return NetworkTablesJNI.setDefaultStringArray(m_handle, time, (String[]) otherValue);
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ * @throws IllegalArgumentException if the value is not a known type
+ */
+ @Override
+ public boolean setDefaultValue(Object defaultValue) {
+ if (defaultValue instanceof NetworkTableValue) {
+ return setDefault((NetworkTableValue) defaultValue);
+ } else if (defaultValue instanceof Boolean) {
+ return setDefaultBoolean((Boolean) defaultValue);
+ } else if (defaultValue instanceof Integer) {
+ return setDefaultInteger((Integer) defaultValue);
+ } else if (defaultValue instanceof Float) {
+ return setDefaultFloat((Float) defaultValue);
+ } else if (defaultValue instanceof Number) {
+ return setDefaultNumber((Number) defaultValue);
+ } else if (defaultValue instanceof String) {
+ return setDefaultString((String) defaultValue);
+ } else if (defaultValue instanceof byte[]) {
+ return setDefaultRaw((byte[]) defaultValue);
+ } else if (defaultValue instanceof boolean[]) {
+ return setDefaultBooleanArray((boolean[]) defaultValue);
+ } else if (defaultValue instanceof long[]) {
+ return setDefaultIntegerArray((long[]) defaultValue);
+ } else if (defaultValue instanceof float[]) {
+ return setDefaultFloatArray((float[]) defaultValue);
+ } else if (defaultValue instanceof double[]) {
+ return setDefaultDoubleArray((double[]) defaultValue);
+ } else if (defaultValue instanceof Boolean[]) {
+ return setDefaultBooleanArray((Boolean[]) defaultValue);
+ } else if (defaultValue instanceof Long[]) {
+ return setDefaultIntegerArray((Long[]) defaultValue);
+ } else if (defaultValue instanceof Float[]) {
+ return setDefaultFloatArray((Float[]) defaultValue);
+ } else if (defaultValue instanceof Number[]) {
+ return setDefaultNumberArray((Number[]) defaultValue);
+ } else if (defaultValue instanceof String[]) {
+ return setDefaultStringArray((String[]) defaultValue);
+ } else {
+ throw new IllegalArgumentException(
+ "Value of type " + defaultValue.getClass().getName() + " cannot be put into a table");
+ }
+ }
+{% for t in types %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ @Override
+ public boolean setDefault{{ t.TypeName }}({{ t.java.ValueType }} defaultValue) {
+ return NetworkTablesJNI.setDefault{{ t.TypeName }}(m_handle, 0, defaultValue);
+ }
+{% if t.java.WrapValueType %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ @Override
+ public boolean setDefault{{ t.TypeName }}({{ t.java.WrapValueType }} defaultValue) {
+ return setDefault{{ t.TypeName }}(NetworkTableValue.toNative{{ t.TypeName }}(defaultValue));
+ }
+{% endif -%}
+{% endfor %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setDefaultNumber(Number defaultValue) {
+ return setDefaultDouble(defaultValue.doubleValue());
+ }
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setDefaultNumberArray(Number[] defaultValue) {
+ return setDefaultDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue));
+ }
+
+ @Override
+ public void unpublish() {
+ NetworkTablesJNI.unpublish(m_handle);
+ }
+
+ private final Topic m_topic;
+}
diff --git a/ntcore/src/generate/java/GenericPublisher.java.jinja b/ntcore/src/generate/java/GenericPublisher.java.jinja
new file mode 100644
index 0000000..d747f17
--- /dev/null
+++ b/ntcore/src/generate/java/GenericPublisher.java.jinja
@@ -0,0 +1,119 @@
+// 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 generic publisher. */
+public interface GenericPublisher extends Publisher, Consumer<NetworkTableValue> {
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ @Override
+ Topic getTopic();
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @return False if the topic already exists with a different type
+ */
+ boolean set(NetworkTableValue value);
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @return False if the topic already exists with a different type
+ * @throws IllegalArgumentException if the value is not a known type
+ */
+ default boolean setValue(Object value) {
+ return setValue(value, 0);
+ }
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @return False if the topic already exists with a different type
+ * @throws IllegalArgumentException if the value is not a known type
+ */
+ boolean setValue(Object value, long time);
+{% for t in types %}
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @return False if the topic already exists with a different type
+ */
+ default boolean set{{ t.TypeName }}({{ t.java.ValueType }} value) {
+ return set{{ t.TypeName }}(value, 0);
+ }
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @return False if the topic already exists with a different type
+ */
+ boolean set{{ t.TypeName }}({{ t.java.ValueType }} value, long time);
+{% if t.java.WrapValueType %}
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @return False if the topic already exists with a different type
+ */
+ default boolean set{{ t.TypeName }}({{ t.java.WrapValueType }} value) {
+ return set{{ t.TypeName }}(value, 0);
+ }
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ * @return False if the topic already exists with a different type
+ */
+ boolean set{{ t.TypeName }}({{ t.java.WrapValueType }} value, long time);
+{% endif -%}
+{% endfor %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ boolean setDefault(NetworkTableValue defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ * @throws IllegalArgumentException if the value is not a known type
+ */
+ boolean setDefaultValue(Object defaultValue);
+{% for t in types %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ boolean setDefault{{ t.TypeName }}({{ t.java.ValueType }} defaultValue);
+{% if t.java.WrapValueType %}
+ boolean setDefault{{ t.TypeName }}({{ t.java.WrapValueType }} defaultValue);
+{% endif -%}
+{% endfor %}
+ @Override
+ default void accept(NetworkTableValue value) {
+ set(value);
+ }
+}
diff --git a/ntcore/src/generate/java/GenericSubscriber.java.jinja b/ntcore/src/generate/java/GenericSubscriber.java.jinja
new file mode 100644
index 0000000..63ecebc
--- /dev/null
+++ b/ntcore/src/generate/java/GenericSubscriber.java.jinja
@@ -0,0 +1,58 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.networktables;
+
+import java.util.function.Supplier;
+
+/** NetworkTables generic subscriber. */
+@SuppressWarnings("PMD.MissingOverride")
+public interface GenericSubscriber extends Subscriber, Supplier<NetworkTableValue> {
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ @Override
+ Topic getTopic();
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns a value with type NetworkTableType.kUnassigned.
+ *
+ * @return value
+ */
+ NetworkTableValue get();
+{% for t in types %}
+ /**
+ * Gets the entry's value as a {{ t.java.ValueType }}. If the entry does not exist or is of different type, it
+ * will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ {{ t.java.ValueType }} get{{ t.TypeName }}({{ t.java.ValueType }} defaultValue);
+{% if t.java.WrapValueType %}
+ /**
+ * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+ * it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ {{ t.java.WrapValueType }} get{{ t.TypeName }}({{ t.java.WrapValueType }} defaultValue);
+{% endif -%}
+{% endfor %}
+ /**
+ * Get an array of all value changes since the last call to readQueue.
+ * Also provides a timestamp for each value.
+ *
+ * <p>The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @return Array of timestamped values; empty array if no new changes have
+ * been published since the previous call.
+ */
+ NetworkTableValue[] readQueue();
+}
diff --git a/ntcore/src/generate/java/NetworkTableEntry.java.jinja b/ntcore/src/generate/java/NetworkTableEntry.java.jinja
new file mode 100644
index 0000000..783ec6f
--- /dev/null
+++ b/ntcore/src/generate/java/NetworkTableEntry.java.jinja
@@ -0,0 +1,537 @@
+// 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 Entry.
+ *
+ * <p>For backwards compatibility, the NetworkTableEntry close() does not release the entry.
+ */
+@SuppressWarnings("UnnecessaryParentheses")
+public final class NetworkTableEntry implements Publisher, Subscriber {
+ /**
+ * Flag values (as returned by {@link #getFlags()}).
+ *
+ * @deprecated Use isPersistent() instead.
+ */
+ @Deprecated(since = "2022", forRemoval = true)
+ public static final int kPersistent = 0x01;
+
+ /**
+ * Construct from native handle.
+ *
+ * @param inst Instance
+ * @param handle Native handle
+ */
+ public NetworkTableEntry(NetworkTableInstance inst, int handle) {
+ this(new Topic(inst, NetworkTablesJNI.getTopicFromHandle(handle)), handle);
+ }
+
+ /**
+ * Construct from native handle.
+ *
+ * @param topic Topic
+ * @param handle Native handle
+ */
+ public NetworkTableEntry(Topic topic, int handle) {
+ m_topic = topic;
+ m_handle = handle;
+ }
+
+ @Override
+ public void close() {}
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ @Override
+ public boolean isValid() {
+ return m_handle != 0;
+ }
+
+ /**
+ * Gets the native handle for the entry.
+ *
+ * @return Native handle
+ */
+ @Override
+ public int getHandle() {
+ return m_handle;
+ }
+
+ /**
+ * Gets the subscribed-to / published-to topic.
+ *
+ * @return Topic
+ */
+ @Override
+ public Topic getTopic() {
+ return m_topic;
+ }
+
+ /**
+ * Gets the instance for the entry.
+ *
+ * @return Instance
+ */
+ public NetworkTableInstance getInstance() {
+ return m_topic.getInstance();
+ }
+
+ /**
+ * Determines if the entry currently exists.
+ *
+ * @return True if the entry exists, false otherwise.
+ */
+ @Override
+ public boolean exists() {
+ return NetworkTablesJNI.getType(m_handle) != 0;
+ }
+
+ /**
+ * Gets the name of the entry (the key).
+ *
+ * @return the entry's name
+ */
+ public String getName() {
+ return NetworkTablesJNI.getEntryName(m_handle);
+ }
+
+ /**
+ * Gets the type of the entry.
+ *
+ * @return the entry's type
+ */
+ public NetworkTableType getType() {
+ return NetworkTableType.getFromInt(NetworkTablesJNI.getType(m_handle));
+ }
+
+ /**
+ * Returns the flags.
+ *
+ * @return the flags (bitmask)
+ * @deprecated Use isPersistent() or topic properties instead
+ */
+ @Deprecated(since = "2022", forRemoval = true)
+ public int getFlags() {
+ return NetworkTablesJNI.getEntryFlags(m_handle);
+ }
+
+ /**
+ * Gets the last time the entry's value was changed.
+ *
+ * @return Entry last change time
+ */
+ @Override
+ public long getLastChange() {
+ return NetworkTablesJNI.getEntryLastChange(m_handle);
+ }
+
+ /**
+ * Gets the entry's value. Returns a value with type NetworkTableType.kUnassigned if the value
+ * does not exist.
+ *
+ * @return the entry's value
+ */
+ public NetworkTableValue getValue() {
+ return NetworkTablesJNI.getValue(m_handle);
+ }
+{% for t in types %}
+ /**
+ * Gets the entry's value as a {{ t.java.ValueType }}. If the entry does not exist or is of different type, it
+ * will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ public {{ t.java.ValueType }} get{{ t.TypeName }}({{ t.java.ValueType }} defaultValue) {
+ return NetworkTablesJNI.get{{ t.TypeName }}(m_handle, defaultValue);
+ }
+{% if t.java.WrapValueType %}
+ /**
+ * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
+ * it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ public {{ t.java.WrapValueType }} get{{ t.TypeName }}({{ t.java.WrapValueType }} defaultValue) {
+ return NetworkTableValue.fromNative{{ t.TypeName }}(
+ get{{ t.TypeName }}(NetworkTableValue.toNative{{ t.TypeName }}(defaultValue)));
+ }
+{% endif -%}
+{% endfor %}
+ /**
+ * Gets the entry's value as a double. If the entry does not exist or is of different type, it
+ * will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ public Number getNumber(Number defaultValue) {
+ return getDouble(defaultValue.doubleValue());
+ }
+
+ /**
+ * Gets the entry's value as a double array. If the entry does not exist or is of different type,
+ * it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ public Number[] getNumberArray(Number[] defaultValue) {
+ return NetworkTableValue.fromNativeDoubleArray(
+ getDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue)));
+ }
+
+ /**
+ * Get an array of all value changes since the last call to readQueue.
+ *
+ * <p>The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @return Array of values; empty array if no new changes have been
+ * published since the previous call.
+ */
+ public NetworkTableValue[] readQueue() {
+ return NetworkTablesJNI.readQueueValue(m_handle);
+ }
+
+ /**
+ * Checks if a data value is of a type that can be placed in a NetworkTable entry.
+ *
+ * @param data the data to check
+ * @return true if the data can be placed in an entry, false if it cannot
+ */
+ public static boolean isValidDataType(Object data) {
+ return data instanceof Number
+ || data instanceof Boolean
+ || data instanceof String
+ || data instanceof long[]
+ || data instanceof Long[]
+ || data instanceof float[]
+ || data instanceof Float[]
+ || data instanceof double[]
+ || data instanceof Double[]
+ || data instanceof Number[]
+ || data instanceof boolean[]
+ || data instanceof Boolean[]
+ || data instanceof String[]
+ || data instanceof byte[]
+ || data instanceof Byte[];
+ }
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ * @throws IllegalArgumentException if the value is not a known type
+ */
+ public boolean setDefaultValue(Object defaultValue) {
+ if (defaultValue instanceof NetworkTableValue) {
+ long time = ((NetworkTableValue) defaultValue).getTime();
+ Object otherValue = ((NetworkTableValue) defaultValue).getValue();
+ switch (((NetworkTableValue) defaultValue).getType()) {
+ case kBoolean:
+ return NetworkTablesJNI.setDefaultBoolean(m_handle, time, (Boolean) otherValue);
+ case kInteger:
+ return NetworkTablesJNI.setDefaultInteger(
+ m_handle, time, ((Number) otherValue).longValue());
+ case kFloat:
+ return NetworkTablesJNI.setDefaultFloat(
+ m_handle, time, ((Number) otherValue).floatValue());
+ case kDouble:
+ return NetworkTablesJNI.setDefaultDouble(
+ m_handle, time, ((Number) otherValue).doubleValue());
+ case kString:
+ return NetworkTablesJNI.setDefaultString(m_handle, time, (String) otherValue);
+ case kRaw:
+ return NetworkTablesJNI.setDefaultRaw(m_handle, time, (byte[]) otherValue);
+ case kBooleanArray:
+ return NetworkTablesJNI.setDefaultBooleanArray(m_handle, time, (boolean[]) otherValue);
+ case kIntegerArray:
+ return NetworkTablesJNI.setDefaultIntegerArray(m_handle, time, (long[]) otherValue);
+ case kFloatArray:
+ return NetworkTablesJNI.setDefaultFloatArray(m_handle, time, (float[]) otherValue);
+ case kDoubleArray:
+ return NetworkTablesJNI.setDefaultDoubleArray(m_handle, time, (double[]) otherValue);
+ case kStringArray:
+ return NetworkTablesJNI.setDefaultStringArray(m_handle, time, (String[]) otherValue);
+ default:
+ return true;
+ }
+ } else if (defaultValue instanceof Boolean) {
+ return setDefaultBoolean((Boolean) defaultValue);
+ } else if (defaultValue instanceof Integer) {
+ return setDefaultInteger((Integer) defaultValue);
+ } else if (defaultValue instanceof Float) {
+ return setDefaultFloat((Float) defaultValue);
+ } else if (defaultValue instanceof Number) {
+ return setDefaultNumber((Number) defaultValue);
+ } else if (defaultValue instanceof String) {
+ return setDefaultString((String) defaultValue);
+ } else if (defaultValue instanceof byte[]) {
+ return setDefaultRaw((byte[]) defaultValue);
+ } else if (defaultValue instanceof boolean[]) {
+ return setDefaultBooleanArray((boolean[]) defaultValue);
+ } else if (defaultValue instanceof long[]) {
+ return setDefaultIntegerArray((long[]) defaultValue);
+ } else if (defaultValue instanceof float[]) {
+ return setDefaultFloatArray((float[]) defaultValue);
+ } else if (defaultValue instanceof double[]) {
+ return setDefaultDoubleArray((double[]) defaultValue);
+ } else if (defaultValue instanceof Boolean[]) {
+ return setDefaultBooleanArray((Boolean[]) defaultValue);
+ } else if (defaultValue instanceof Long[]) {
+ return setDefaultIntegerArray((Long[]) defaultValue);
+ } else if (defaultValue instanceof Float[]) {
+ return setDefaultFloatArray((Float[]) defaultValue);
+ } else if (defaultValue instanceof Number[]) {
+ return setDefaultNumberArray((Number[]) defaultValue);
+ } else if (defaultValue instanceof String[]) {
+ return setDefaultStringArray((String[]) defaultValue);
+ } else {
+ throw new IllegalArgumentException(
+ "Value of type " + defaultValue.getClass().getName() + " cannot be put into a table");
+ }
+ }
+{% for t in types %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setDefault{{ t.TypeName }}({{ t.java.ValueType }} defaultValue) {
+ return NetworkTablesJNI.setDefault{{ t.TypeName }}(m_handle, 0, defaultValue);
+ }
+{% if t.java.WrapValueType %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setDefault{{ t.TypeName }}({{ t.java.WrapValueType }} defaultValue) {
+ return setDefault{{ t.TypeName }}(NetworkTableValue.toNative{{ t.TypeName }}(defaultValue));
+ }
+{% endif -%}
+{% endfor %}
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setDefaultNumber(Number defaultValue) {
+ return setDefaultDouble(defaultValue.doubleValue());
+ }
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setDefaultNumberArray(Number[] defaultValue) {
+ return setDefaultDoubleArray(NetworkTableValue.toNativeDoubleArray(defaultValue));
+ }
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value that will be assigned
+ * @return False if the table key already exists with a different type
+ * @throws IllegalArgumentException if the value is not a known type
+ */
+ public boolean setValue(Object value) {
+ if (value instanceof NetworkTableValue) {
+ long time = ((NetworkTableValue) value).getTime();
+ Object otherValue = ((NetworkTableValue) value).getValue();
+ switch (((NetworkTableValue) value).getType()) {
+ case kBoolean:
+ return NetworkTablesJNI.setBoolean(m_handle, time, (Boolean) otherValue);
+ case kInteger:
+ return NetworkTablesJNI.setInteger(
+ m_handle, time, ((Number) otherValue).longValue());
+ case kFloat:
+ return NetworkTablesJNI.setFloat(
+ m_handle, time, ((Number) otherValue).floatValue());
+ case kDouble:
+ return NetworkTablesJNI.setDouble(
+ m_handle, time, ((Number) otherValue).doubleValue());
+ case kString:
+ return NetworkTablesJNI.setString(m_handle, time, (String) otherValue);
+ case kRaw:
+ return NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue);
+ case kBooleanArray:
+ return NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue);
+ case kIntegerArray:
+ return NetworkTablesJNI.setIntegerArray(m_handle, time, (long[]) otherValue);
+ case kFloatArray:
+ return NetworkTablesJNI.setFloatArray(m_handle, time, (float[]) otherValue);
+ case kDoubleArray:
+ return NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue);
+ case kStringArray:
+ return NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue);
+ default:
+ return true;
+ }
+ } else if (value instanceof Boolean) {
+ return setBoolean((Boolean) value);
+ } else if (value instanceof Long) {
+ return setInteger((Long) value);
+ } else if (value instanceof Float) {
+ return setFloat((Float) value);
+ } else if (value instanceof Number) {
+ return setNumber((Number) value);
+ } else if (value instanceof String) {
+ return setString((String) value);
+ } else if (value instanceof byte[]) {
+ return setRaw((byte[]) value);
+ } else if (value instanceof boolean[]) {
+ return setBooleanArray((boolean[]) value);
+ } else if (value instanceof long[]) {
+ return setIntegerArray((long[]) value);
+ } else if (value instanceof float[]) {
+ return setFloatArray((float[]) value);
+ } else if (value instanceof double[]) {
+ return setDoubleArray((double[]) value);
+ } else if (value instanceof Boolean[]) {
+ return setBooleanArray((Boolean[]) value);
+ } else if (value instanceof Long[]) {
+ return setIntegerArray((Long[]) value);
+ } else if (value instanceof Float[]) {
+ return setFloatArray((Float[]) value);
+ } else if (value instanceof Number[]) {
+ return setNumberArray((Number[]) value);
+ } else if (value instanceof String[]) {
+ return setStringArray((String[]) value);
+ } else {
+ throw new IllegalArgumentException(
+ "Value of type " + value.getClass().getName() + " cannot be put into a table");
+ }
+ }
+{% for t in types %}
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean set{{ t.TypeName }}({{ t.java.ValueType }} value) {
+ return NetworkTablesJNI.set{{ t.TypeName }}(m_handle, 0, value);
+ }
+{% if t.java.WrapValueType %}
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean set{{ t.TypeName }}({{ t.java.WrapValueType }} value) {
+ return set{{ t.TypeName }}(NetworkTableValue.toNative{{ t.TypeName }}(value));
+ }
+{% endif -%}
+{% endfor %}
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setNumber(Number value) {
+ return setDouble(value.doubleValue());
+ }
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @return False if the entry exists with a different type
+ */
+ public boolean setNumberArray(Number[] value) {
+ return setDoubleArray(NetworkTableValue.toNativeDoubleArray(value));
+ }
+
+ /**
+ * Sets flags.
+ *
+ * @param flags the flags to set (bitmask)
+ * @deprecated Use setPersistent() or topic properties instead
+ */
+ @Deprecated(since = "2022", forRemoval = true)
+ public void setFlags(int flags) {
+ NetworkTablesJNI.setEntryFlags(m_handle, getFlags() | flags);
+ }
+
+ /**
+ * Clears flags.
+ *
+ * @param flags the flags to clear (bitmask)
+ * @deprecated Use setPersistent() or topic properties instead
+ */
+ @Deprecated(since = "2022", forRemoval = true)
+ public void clearFlags(int flags) {
+ NetworkTablesJNI.setEntryFlags(m_handle, getFlags() & ~flags);
+ }
+
+ /** Make value persistent through program restarts. */
+ public void setPersistent() {
+ NetworkTablesJNI.setTopicPersistent(m_topic.getHandle(), true);
+ }
+
+ /** Stop making value persistent through program restarts. */
+ public void clearPersistent() {
+ NetworkTablesJNI.setTopicPersistent(m_topic.getHandle(), false);
+ }
+
+ /**
+ * Returns whether the value is persistent through program restarts.
+ *
+ * @return True if the value is persistent.
+ */
+ public boolean isPersistent() {
+ return NetworkTablesJNI.getTopicPersistent(m_topic.getHandle());
+ }
+
+ /** Stops publishing the entry if it's been published. */
+ public void unpublish() {
+ NetworkTablesJNI.unpublish(m_handle);
+ }
+
+ /**
+ * Deletes the entry.
+ *
+ * @deprecated Use unpublish() instead.
+ */
+ @Deprecated(since = "2022", forRemoval = true)
+ public void delete() {
+ unpublish();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof NetworkTableEntry)) {
+ return false;
+ }
+
+ return m_handle == ((NetworkTableEntry) other).m_handle;
+ }
+
+ @Override
+ public int hashCode() {
+ return m_handle;
+ }
+
+ private final Topic m_topic;
+ protected int m_handle;
+}
diff --git a/ntcore/src/generate/java/NetworkTableInstance.java.jinja b/ntcore/src/generate/java/NetworkTableInstance.java.jinja
new file mode 100644
index 0000000..ba5e33f
--- /dev/null
+++ b/ntcore/src/generate/java/NetworkTableInstance.java.jinja
@@ -0,0 +1,1064 @@
+// 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.WPIUtilJNI;
+import edu.wpi.first.util.concurrent.Event;
+import edu.wpi.first.util.datalog.DataLog;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.OptionalLong;
+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;
+
+/**
+ * NetworkTables Instance.
+ *
+ * <p>Instances are completely independent from each other. Table operations on one instance will
+ * not be visible to other instances unless the instances are connected via the network. The main
+ * limitation on instances is that you cannot have two servers on the same network port. The main
+ * utility of instances is for unit testing, but they can also enable one program to connect to two
+ * different NetworkTables networks.
+ *
+ * <p>The global "default" instance (as returned by {@link #getDefault()}) is always available, and
+ * is intended for the common case when there is only a single NetworkTables instance being used in
+ * the program.
+ *
+ * <p>Additional instances can be created with the {@link #create()} function. A reference must be
+ * kept to the NetworkTableInstance returned by this function to keep it from being garbage
+ * collected.
+ */
+@SuppressWarnings("PMD.CouplingBetweenObjects")
+public final class NetworkTableInstance implements AutoCloseable {
+ /** Client/server mode flag values (as returned by {@link #getNetworkMode()}). */
+ public enum NetworkMode {
+ /** Running in server mode. */
+ kServer(0x01),
+
+ /** Running in NT3 client mode. */
+ kClient3(0x02),
+
+ /** Running in NT4 client mode. */
+ kClient4(0x04),
+
+ /** Currently starting up (either client or server). */
+ kStarting(0x08),
+
+ /** Running in local-only mode. */
+ kLocal(0x10);
+
+ private final int value;
+
+ NetworkMode(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ /** The default port that network tables operates on for NT3. */
+ public static final int kDefaultPort3 = 1735;
+
+ /** The default port that network tables operates on for NT4. */
+ public static final int kDefaultPort4 = 5810;
+
+ /**
+ * Construct from native handle.
+ *
+ * @param handle Native handle
+ */
+ private NetworkTableInstance(int handle) {
+ m_owned = false;
+ m_handle = handle;
+ }
+
+ /** Destroys the instance (if created by {@link #create()}). */
+ @Override
+ public synchronized void close() {
+ if (m_owned && m_handle != 0) {
+ m_listeners.close();
+ NetworkTablesJNI.destroyInstance(m_handle);
+ m_handle = 0;
+ }
+ }
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ public boolean isValid() {
+ return m_handle != 0;
+ }
+
+ /* The default instance. */
+ private static NetworkTableInstance s_defaultInstance;
+
+ /**
+ * Get global default instance.
+ *
+ * @return Global default instance
+ */
+ public static synchronized NetworkTableInstance getDefault() {
+ if (s_defaultInstance == null) {
+ s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance());
+ }
+ return s_defaultInstance;
+ }
+
+ /**
+ * Create an instance. Note: A reference to the returned instance must be retained to ensure the
+ * instance is not garbage collected.
+ *
+ * @return Newly created instance
+ */
+ public static NetworkTableInstance create() {
+ NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance());
+ inst.m_owned = true;
+ return inst;
+ }
+
+ /**
+ * Gets the native handle for the instance.
+ *
+ * @return Native handle
+ */
+ public int getHandle() {
+ return m_handle;
+ }
+
+ /**
+ * Get (generic) topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ public Topic getTopic(String name) {
+ Topic topic = m_topics.get(name);
+ if (topic == null) {
+ int handle = NetworkTablesJNI.getTopic(m_handle, name);
+ topic = new Topic(this, handle);
+ Topic oldTopic = m_topics.putIfAbsent(name, topic);
+ if (oldTopic != null) {
+ topic = oldTopic;
+ }
+ // also cache by handle
+ m_topicsByHandle.putIfAbsent(handle, topic);
+ }
+ return topic;
+ }
+{% for t in types %}
+ /**
+ * Get {{ t.java.ValueType }} topic.
+ *
+ * @param name topic name
+ * @return {{ t.TypeName }}Topic
+ */
+ public {{ t.TypeName }}Topic get{{ t.TypeName }}Topic(String name) {
+ Topic topic = m_topics.get(name);
+ if (topic instanceof {{ t.TypeName }}Topic) {
+ return ({{ t.TypeName }}Topic) topic;
+ }
+
+ int handle;
+ if (topic == null) {
+ handle = NetworkTablesJNI.getTopic(m_handle, name);
+ } else {
+ handle = topic.getHandle();
+ }
+
+ topic = new {{ t.TypeName }}Topic(this, handle);
+ m_topics.put(name, topic);
+
+ // also cache by handle
+ m_topicsByHandle.put(handle, topic);
+
+ return ({{ t.TypeName }}Topic) topic;
+ }
+{% endfor %}
+ private Topic[] topicHandlesToTopics(int[] handles) {
+ Topic[] topics = new Topic[handles.length];
+ for (int i = 0; i < handles.length; i++) {
+ topics[i] = getCachedTopic(handles[i]);
+ }
+ return topics;
+ }
+
+ /**
+ * Get all published topics.
+ *
+ * @return Array of topics.
+ */
+ public Topic[] getTopics() {
+ return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, "", 0));
+ }
+
+ /**
+ * Get published topics starting with the given prefix. The results are optionally filtered by
+ * string prefix to only return a subset of all topics.
+ *
+ * @param prefix topic name required prefix; only topics whose name starts with this string are
+ * returned
+ * @return Array of topic information.
+ */
+ public Topic[] getTopics(String prefix) {
+ return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, 0));
+ }
+
+ /**
+ * Get published topics starting with the given prefix. The results are optionally filtered by
+ * string prefix and data type to only return a subset of all topics.
+ *
+ * @param prefix topic name required prefix; only topics whose name starts with this string are
+ * returned
+ * @param types bitmask of data types; 0 is treated as a "don't care"
+ * @return Array of topic information.
+ */
+ public Topic[] getTopics(String prefix, int types) {
+ return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, types));
+ }
+
+ /**
+ * Get published topics starting with the given prefix. The results are optionally filtered by
+ * string prefix and data type to only return a subset of all topics.
+ *
+ * @param prefix topic name required prefix; only topics whose name starts with this string are
+ * returned
+ * @param types array of data type strings
+ * @return Array of topic information.
+ */
+ public Topic[] getTopics(String prefix, String[] types) {
+ return topicHandlesToTopics(NetworkTablesJNI.getTopicsStr(m_handle, prefix, types));
+ }
+
+ /**
+ * Get information about all topics.
+ *
+ * @return Array of topic information.
+ */
+ public TopicInfo[] getTopicInfo() {
+ return NetworkTablesJNI.getTopicInfos(this, m_handle, "", 0);
+ }
+
+ /**
+ * Get information about topics starting with the given prefix. The results are optionally
+ * filtered by string prefix to only return a subset of all topics.
+ *
+ * @param prefix topic name required prefix; only topics whose name starts with this string are
+ * returned
+ * @return Array of topic information.
+ */
+ public TopicInfo[] getTopicInfo(String prefix) {
+ return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, 0);
+ }
+
+ /**
+ * Get information about topics starting with the given prefix. The results are optionally
+ * filtered by string prefix and data type to only return a subset of all topics.
+ *
+ * @param prefix topic name required prefix; only topics whose name starts with this string are
+ * returned
+ * @param types bitmask of data types; 0 is treated as a "don't care"
+ * @return Array of topic information.
+ */
+ public TopicInfo[] getTopicInfo(String prefix, int types) {
+ return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, types);
+ }
+
+ /**
+ * Get information about topics starting with the given prefix. The results are optionally
+ * filtered by string prefix and data type to only return a subset of all topics.
+ *
+ * @param prefix topic name required prefix; only topics whose name starts with this string are
+ * returned
+ * @param types array of data type strings
+ * @return Array of topic information.
+ */
+ public TopicInfo[] getTopicInfo(String prefix, String[] types) {
+ return NetworkTablesJNI.getTopicInfosStr(this, m_handle, prefix, types);
+ }
+
+ /* Cache of created entries. */
+ private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>();
+
+ /**
+ * Gets the entry for a key.
+ *
+ * @param name Key
+ * @return Network table entry.
+ */
+ public NetworkTableEntry getEntry(String name) {
+ NetworkTableEntry entry = m_entries.get(name);
+ if (entry == null) {
+ entry = new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name));
+ NetworkTableEntry oldEntry = m_entries.putIfAbsent(name, entry);
+ if (oldEntry != null) {
+ entry = oldEntry;
+ }
+ }
+ return entry;
+ }
+
+ /* Cache of created topics. */
+ private final ConcurrentMap<String, Topic> m_topics = new ConcurrentHashMap<>();
+ private final ConcurrentMap<Integer, Topic> m_topicsByHandle = new ConcurrentHashMap<>();
+
+ Topic getCachedTopic(String name) {
+ Topic topic = m_topics.get(name);
+ if (topic == null) {
+ int handle = NetworkTablesJNI.getTopic(m_handle, name);
+ topic = new Topic(this, handle);
+ Topic oldTopic = m_topics.putIfAbsent(name, topic);
+ if (oldTopic != null) {
+ topic = oldTopic;
+ }
+ // also cache by handle
+ m_topicsByHandle.putIfAbsent(handle, topic);
+ }
+ return topic;
+ }
+
+ Topic getCachedTopic(int handle) {
+ Topic topic = m_topicsByHandle.get(handle);
+ if (topic == null) {
+ topic = new Topic(this, handle);
+ Topic oldTopic = m_topicsByHandle.putIfAbsent(handle, topic);
+ if (oldTopic != null) {
+ topic = oldTopic;
+ }
+ }
+ return topic;
+ }
+
+ /* Cache of created tables. */
+ private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>();
+
+ /**
+ * Gets the table with the specified key.
+ *
+ * @param key the key name
+ * @return The network table
+ */
+ public NetworkTable getTable(String key) {
+ // prepend leading / if not present
+ String theKey;
+ if (key.isEmpty() || "/".equals(key)) {
+ theKey = "";
+ } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
+ theKey = key;
+ } else {
+ theKey = NetworkTable.PATH_SEPARATOR + key;
+ }
+
+ // cache created tables
+ NetworkTable table = m_tables.get(theKey);
+ if (table == null) {
+ table = new NetworkTable(this, theKey);
+ NetworkTable oldTable = m_tables.putIfAbsent(theKey, table);
+ if (oldTable != null) {
+ table = oldTable;
+ }
+ }
+ return table;
+ }
+
+ /*
+ * Callback Creation Functions
+ */
+
+ private static class ListenerStorage implements AutoCloseable {
+ private final ReentrantLock m_lock = new ReentrantLock();
+ private final Map<Integer, Consumer<NetworkTableEvent>> m_listeners = new HashMap<>();
+ private Thread m_thread;
+ private int m_poller;
+ private boolean m_waitQueue;
+ private final Event m_waitQueueEvent = new Event();
+ private final Condition m_waitQueueCond = m_lock.newCondition();
+ private final NetworkTableInstance m_inst;
+
+ ListenerStorage(NetworkTableInstance inst) {
+ m_inst = inst;
+ }
+
+ int add(
+ String[] prefixes,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ m_lock.lock();
+ try {
+ if (m_poller == 0) {
+ m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
+ startThread();
+ }
+ int h = NetworkTablesJNI.addListener(m_poller, prefixes, eventKinds);
+ m_listeners.put(h, listener);
+ return h;
+ } finally {
+ m_lock.unlock();
+ }
+ }
+
+ int add(
+ int handle,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ m_lock.lock();
+ try {
+ if (m_poller == 0) {
+ m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
+ startThread();
+ }
+ int h = NetworkTablesJNI.addListener(m_poller, handle, eventKinds);
+ m_listeners.put(h, listener);
+ return h;
+ } finally {
+ m_lock.unlock();
+ }
+ }
+
+ int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> listener) {
+ m_lock.lock();
+ try {
+ if (m_poller == 0) {
+ m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
+ startThread();
+ }
+ int h = NetworkTablesJNI.addLogger(m_poller, minLevel, maxLevel);
+ m_listeners.put(h, listener);
+ return h;
+ } finally {
+ m_lock.unlock();
+ }
+ }
+
+ void remove(int listener) {
+ m_lock.lock();
+ try {
+ m_listeners.remove(listener);
+ } finally {
+ m_lock.unlock();
+ }
+ NetworkTablesJNI.removeListener(listener);
+ }
+
+ @Override
+ public void close() {
+ if (m_poller != 0) {
+ NetworkTablesJNI.destroyListenerPoller(m_poller);
+ }
+ m_poller = 0;
+ }
+
+ private void startThread() {
+ m_thread =
+ new Thread(
+ () -> {
+ boolean wasInterrupted = false;
+ int[] handles = new int[] { m_poller, m_waitQueueEvent.getHandle() };
+ while (!Thread.interrupted()) {
+ try {
+ WPIUtilJNI.waitForObjects(handles);
+ } catch (InterruptedException ex) {
+ m_lock.lock();
+ try {
+ if (m_waitQueue) {
+ m_waitQueue = false;
+ m_waitQueueCond.signalAll();
+ }
+ } finally {
+ m_lock.unlock();
+ }
+ Thread.currentThread().interrupt();
+ // don't try to destroy poller, as its handle is likely no longer valid
+ wasInterrupted = true;
+ break;
+ }
+ for (NetworkTableEvent event :
+ NetworkTablesJNI.readListenerQueue(m_inst, m_poller)) {
+ Consumer<NetworkTableEvent> listener;
+ m_lock.lock();
+ try {
+ listener = m_listeners.get(event.listener);
+ } finally {
+ m_lock.unlock();
+ }
+ if (listener != null) {
+ try {
+ listener.accept(event);
+ } catch (Throwable throwable) {
+ System.err.println(
+ "Unhandled exception during listener callback: "
+ + throwable.toString());
+ throwable.printStackTrace();
+ }
+ }
+ }
+ m_lock.lock();
+ try {
+ if (m_waitQueue) {
+ m_waitQueue = false;
+ m_waitQueueCond.signalAll();
+ }
+ } finally {
+ m_lock.unlock();
+ }
+ }
+ m_lock.lock();
+ try {
+ if (!wasInterrupted) {
+ NetworkTablesJNI.destroyListenerPoller(m_poller);
+ }
+ m_poller = 0;
+ } finally {
+ m_lock.unlock();
+ }
+ },
+ "NTListener");
+ m_thread.setDaemon(true);
+ m_thread.start();
+ }
+
+ boolean waitForQueue(double timeout) {
+ m_lock.lock();
+ try {
+ if (m_poller != 0) {
+ m_waitQueue = true;
+ m_waitQueueEvent.set();
+ while (m_waitQueue) {
+ try {
+ if (timeout < 0) {
+ m_waitQueueCond.await();
+ } else {
+ return m_waitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
+ }
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ return true;
+ }
+ }
+ }
+ } finally {
+ m_lock.unlock();
+ }
+ return true;
+ }
+ }
+
+ private ListenerStorage m_listeners = new ListenerStorage(this);
+
+ /**
+ * Remove a connection listener.
+ *
+ * @param listener Listener handle to remove
+ */
+ public void removeListener(int listener) {
+ m_listeners.remove(listener);
+ }
+
+ /**
+ * Wait for the listener queue to be empty. This is primarily useful for deterministic
+ * testing. This blocks until either the listener queue is empty (e.g. there are no
+ * more events that need to be passed along to callbacks or poll queues) or the timeout expires.
+ *
+ * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
+ * block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+ public boolean waitForListenerQueue(double timeout) {
+ return m_listeners.waitForQueue(timeout);
+ }
+
+ /**
+ * Add a connection listener. The callback function is called asynchronously on a separate
+ * thread, so it's important to use synchronization or atomics when accessing any shared state
+ * from the callback function.
+ *
+ * @param immediateNotify Notify listener of all existing connections
+ * @param listener Listener to add
+ * @return Listener handle
+ */
+ public int addConnectionListener(
+ boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
+ EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kConnection);
+ if (immediateNotify) {
+ eventKinds.add(NetworkTableEvent.Kind.kImmediate);
+ }
+ return m_listeners.add(m_handle, eventKinds, listener);
+ }
+
+ /**
+ * Add a time synchronization listener. The callback function is called asynchronously on a
+ * separate thread, so it's important to use synchronization or atomics when accessing any shared
+ * state from the callback function.
+ *
+ * @param immediateNotify Notify listener of current time synchronization value
+ * @param listener Listener to add
+ * @return Listener handle
+ */
+ public int addTimeSyncListener(
+ boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
+ EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kTimeSync);
+ if (immediateNotify) {
+ eventKinds.add(NetworkTableEvent.Kind.kImmediate);
+ }
+ return m_listeners.add(m_handle, eventKinds, listener);
+ }
+
+ /**
+ * Add a listener for changes on a particular topic. The callback function is called
+ * asynchronously on a separate thread, so it's important to use synchronization or atomics when
+ * accessing any shared state from the callback function.
+ *
+ * <p>This creates a corresponding internal subscriber with the lifetime of the
+ * listener.
+ *
+ * @param topic Topic
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ public int addListener(
+ Topic topic,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ if (topic.getInstance().getHandle() != m_handle) {
+ throw new IllegalArgumentException("topic is not from this instance");
+ }
+ return m_listeners.add(topic.getHandle(), eventKinds, listener);
+ }
+
+ /**
+ * Add a listener for changes on a subscriber. The callback function is called
+ * asynchronously on a separate thread, so it's important to use synchronization or atomics when
+ * accessing any shared state from the callback function. This does NOT keep the subscriber
+ * active.
+ *
+ * @param subscriber Subscriber
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ public int addListener(
+ Subscriber subscriber,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ if (subscriber.getTopic().getInstance().getHandle() != m_handle) {
+ throw new IllegalArgumentException("subscriber is not from this instance");
+ }
+ return m_listeners.add(subscriber.getHandle(), eventKinds, listener);
+ }
+
+ /**
+ * Add a listener for changes on a subscriber. The callback function is called
+ * asynchronously on a separate thread, so it's important to use synchronization or atomics when
+ * accessing any shared state from the callback function. This does NOT keep the subscriber
+ * active.
+ *
+ * @param subscriber Subscriber
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ public int addListener(
+ MultiSubscriber subscriber,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ if (subscriber.getInstance().getHandle() != m_handle) {
+ throw new IllegalArgumentException("subscriber is not from this instance");
+ }
+ return m_listeners.add(subscriber.getHandle(), eventKinds, listener);
+ }
+
+ /**
+ * Add a listener for changes on an entry. The callback function is called
+ * asynchronously on a separate thread, so it's important to use synchronization or atomics when
+ * accessing any shared state from the callback function.
+ *
+ * @param entry Entry
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ public int addListener(
+ NetworkTableEntry entry,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ if (entry.getTopic().getInstance().getHandle() != m_handle) {
+ throw new IllegalArgumentException("entry is not from this instance");
+ }
+ return m_listeners.add(entry.getHandle(), eventKinds, listener);
+ }
+
+ /**
+ * Add a listener for changes to topics with names that start with any of the given
+ * prefixes. The callback function is called asynchronously on a separate thread, so it's
+ * important to use synchronization or atomics when accessing any shared state from the callback
+ * function.
+ *
+ * <p>This creates a corresponding internal subscriber with the lifetime of the
+ * listener.
+ *
+ * @param prefixes Topic name string prefixes
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ public int addListener(
+ String[] prefixes,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ return m_listeners.add(prefixes, eventKinds, listener);
+ }
+
+ /*
+ * Client/Server Functions
+ */
+
+ /**
+ * Get the current network mode.
+ *
+ * @return Enum set of NetworkMode.
+ */
+ public EnumSet<NetworkMode> getNetworkMode() {
+ int flags = NetworkTablesJNI.getNetworkMode(m_handle);
+ EnumSet<NetworkMode> rv = EnumSet.noneOf(NetworkMode.class);
+ for (NetworkMode mode : NetworkMode.values()) {
+ if ((flags & mode.getValue()) != 0) {
+ rv.add(mode);
+ }
+ }
+ return rv;
+ }
+
+ /**
+ * Starts local-only operation. Prevents calls to startServer or startClient from taking effect.
+ * Has no effect if startServer or startClient has already been called.
+ */
+ public void startLocal() {
+ NetworkTablesJNI.startLocal(m_handle);
+ }
+
+ /**
+ * Stops local-only operation. startServer or startClient can be called after this call to start
+ * a server or client.
+ */
+ public void stopLocal() {
+ NetworkTablesJNI.stopLocal(m_handle);
+ }
+
+ /**
+ * Starts a server using the networktables.json as the persistent file, using the default
+ * listening address and port.
+ */
+ public void startServer() {
+ startServer("networktables.json");
+ }
+
+ /**
+ * Starts a server using the specified persistent filename, using the default listening address
+ * and port.
+ *
+ * @param persistFilename the name of the persist file to use
+ */
+ public void startServer(String persistFilename) {
+ startServer(persistFilename, "");
+ }
+
+ /**
+ * Starts a server using the specified filename and listening address, using the default port.
+ *
+ * @param persistFilename the name of the persist file to use
+ * @param listenAddress the address to listen on, or empty to listen on any address
+ */
+ public void startServer(String persistFilename, String listenAddress) {
+ startServer(persistFilename, listenAddress, kDefaultPort3, kDefaultPort4);
+ }
+
+ /**
+ * Starts a server using the specified filename, listening address, and port.
+ *
+ * @param persistFilename the name of the persist file to use
+ * @param listenAddress the address to listen on, or empty to listen on any address
+ * @param port3 port to communicate over (NT3)
+ */
+ public void startServer(String persistFilename, String listenAddress, int port3) {
+ startServer(persistFilename, listenAddress, port3, kDefaultPort4);
+ }
+
+ /**
+ * Starts a server using the specified filename, listening address, and port.
+ *
+ * @param persistFilename the name of the persist file to use
+ * @param listenAddress the address to listen on, or empty to listen on any address
+ * @param port3 port to communicate over (NT3)
+ * @param port4 port to communicate over (NT4)
+ */
+ public void startServer(String persistFilename, String listenAddress, int port3, int port4) {
+ NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port3, port4);
+ }
+
+ /** Stops the server if it is running. */
+ public void stopServer() {
+ NetworkTablesJNI.stopServer(m_handle);
+ }
+
+ /**
+ * Starts a NT3 client. Use SetServer or SetServerTeam to set the server name and port.
+ *
+ * @param identity network identity to advertise (cannot be empty string)
+ */
+ public void startClient3(String identity) {
+ NetworkTablesJNI.startClient3(m_handle, identity);
+ }
+
+ /**
+ * Starts a NT4 client. Use SetServer or SetServerTeam to set the server name and port.
+ *
+ * @param identity network identity to advertise (cannot be empty string)
+ */
+ public void startClient4(String identity) {
+ NetworkTablesJNI.startClient4(m_handle, identity);
+ }
+
+ /** Stops the client if it is running. */
+ public void stopClient() {
+ NetworkTablesJNI.stopClient(m_handle);
+ }
+
+ /**
+ * Sets server address and port for client (without restarting client). Changes the port to the
+ * default port.
+ *
+ * @param serverName server name
+ */
+ public void setServer(String serverName) {
+ setServer(serverName, 0);
+ }
+
+ /**
+ * Sets server address and port for client (without restarting client).
+ *
+ * @param serverName server name
+ * @param port port to communicate over (0=default)
+ */
+ public void setServer(String serverName, int port) {
+ NetworkTablesJNI.setServer(m_handle, serverName, port);
+ }
+
+ /**
+ * Sets server addresses and port for client (without restarting client). Changes the port to the
+ * default port. The client will attempt to connect to each server in round robin fashion.
+ *
+ * @param serverNames array of server names
+ */
+ public void setServer(String[] serverNames) {
+ setServer(serverNames, 0);
+ }
+
+ /**
+ * Sets server addresses and port for client (without restarting client). The client will attempt
+ * to connect to each server in round robin fashion.
+ *
+ * @param serverNames array of server names
+ * @param port port to communicate over (0=default)
+ */
+ public void setServer(String[] serverNames, int port) {
+ int[] ports = new int[serverNames.length];
+ for (int i = 0; i < serverNames.length; i++) {
+ ports[i] = port;
+ }
+ setServer(serverNames, ports);
+ }
+
+ /**
+ * Sets server addresses and ports for client (without restarting client). The client will
+ * attempt to connect to each server in round robin fashion.
+ *
+ * @param serverNames array of server names
+ * @param ports array of port numbers (0=default)
+ */
+ public void setServer(String[] serverNames, int[] ports) {
+ NetworkTablesJNI.setServer(m_handle, serverNames, ports);
+ }
+
+ /**
+ * Sets server addresses and port for client (without restarting client). Changes the port to the
+ * default port. The client will attempt to connect to each server in round robin fashion.
+ *
+ * @param team team number
+ */
+ public void setServerTeam(int team) {
+ setServerTeam(team, 0);
+ }
+
+ /**
+ * Sets server addresses and port for client (without restarting client). Connects using commonly
+ * known robot addresses for the specified team.
+ *
+ * @param team team number
+ * @param port port to communicate over (0=default)
+ */
+ public void setServerTeam(int team, int port) {
+ NetworkTablesJNI.setServerTeam(m_handle, team, port);
+ }
+
+ /**
+ * Starts requesting server address from Driver Station. This connects to the Driver Station
+ * running on localhost to obtain the server IP address, and connects with the default port.
+ */
+ public void startDSClient() {
+ startDSClient(0);
+ }
+
+ /**
+ * Starts requesting server address from Driver Station. This connects to the Driver Station
+ * running on localhost to obtain the server IP address.
+ *
+ * @param port server port to use in combination with IP from DS (0=default)
+ */
+ public void startDSClient(int port) {
+ NetworkTablesJNI.startDSClient(m_handle, port);
+ }
+
+ /** Stops requesting server address from Driver Station. */
+ public void stopDSClient() {
+ NetworkTablesJNI.stopDSClient(m_handle);
+ }
+
+ /**
+ * Flushes all updated values immediately to the local client/server. This does not flush to the
+ * network.
+ */
+ public void flushLocal() {
+ NetworkTablesJNI.flushLocal(m_handle);
+ }
+
+ /**
+ * Flushes all updated values immediately to the network. Note: This is rate-limited to protect
+ * the network from flooding. This is primarily useful for synchronizing network updates with
+ * user code.
+ */
+ public void flush() {
+ NetworkTablesJNI.flush(m_handle);
+ }
+
+ /**
+ * Gets information on the currently established network connections. If operating as a client,
+ * this will return either zero or one values.
+ *
+ * @return array of connection information
+ */
+ public ConnectionInfo[] getConnections() {
+ return NetworkTablesJNI.getConnections(m_handle);
+ }
+
+ /**
+ * Return whether or not the instance is connected to another node.
+ *
+ * @return True if connected.
+ */
+ public boolean isConnected() {
+ return NetworkTablesJNI.isConnected(m_handle);
+ }
+
+ /**
+ * Get the time offset between server time and local time. Add this value to local time to get
+ * the estimated equivalent server time. In server mode, this always returns 0. In client mode,
+ * this returns the time offset only if the client and server are connected and have exchanged
+ * synchronization messages. Note the time offset may change over time as it is periodically
+ * updated; to receive updates as events, add a listener to the "time sync" event.
+ *
+ * @return Time offset in microseconds (optional)
+ */
+ public OptionalLong getServerTimeOffset() {
+ return NetworkTablesJNI.getServerTimeOffset(m_handle);
+ }
+
+ /**
+ * Starts logging entry changes to a DataLog.
+ *
+ * @param log data log object; lifetime must extend until StopEntryDataLog is called or the
+ * instance is destroyed
+ * @param prefix only store entries with names that start with this prefix; the prefix is not
+ * included in the data log entry name
+ * @param logPrefix prefix to add to data log entry names
+ * @return Data logger handle
+ */
+ public int startEntryDataLog(DataLog log, String prefix, String logPrefix) {
+ return NetworkTablesJNI.startEntryDataLog(m_handle, log, prefix, logPrefix);
+ }
+
+ /**
+ * Stops logging entry changes to a DataLog.
+ *
+ * @param logger data logger handle
+ */
+ public static void stopEntryDataLog(int logger) {
+ NetworkTablesJNI.stopEntryDataLog(logger);
+ }
+
+ /**
+ * Starts logging connection changes to a DataLog.
+ *
+ * @param log data log object; lifetime must extend until StopConnectionDataLog is called or the
+ * instance is destroyed
+ * @param name data log entry name
+ * @return Data logger handle
+ */
+ public int startConnectionDataLog(DataLog log, String name) {
+ return NetworkTablesJNI.startConnectionDataLog(m_handle, log, name);
+ }
+
+ /**
+ * Stops logging connection changes to a DataLog.
+ *
+ * @param logger data logger handle
+ */
+ public static void stopConnectionDataLog(int logger) {
+ NetworkTablesJNI.stopConnectionDataLog(logger);
+ }
+
+ /**
+ * Add logger callback function. By default, log messages are sent to stderr; this function sends
+ * log messages with the specified levels to the provided callback function instead. The callback
+ * function will only be called for log messages with level greater than or equal to minLevel and
+ * less than or equal to maxLevel; messages outside this range will be silently ignored.
+ *
+ * @param minLevel minimum log level
+ * @param maxLevel maximum log level
+ * @param func callback function
+ * @return Listener handle
+ */
+ public int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> func) {
+ return m_listeners.addLogger(minLevel, maxLevel, func);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof NetworkTableInstance)) {
+ return false;
+ }
+
+ return m_handle == ((NetworkTableInstance) other).m_handle;
+ }
+
+ @Override
+ public int hashCode() {
+ return m_handle;
+ }
+
+ private boolean m_owned;
+ private int m_handle;
+}
diff --git a/ntcore/src/generate/java/NetworkTableValue.java.jinja b/ntcore/src/generate/java/NetworkTableValue.java.jinja
new file mode 100644
index 0000000..d2c8d11
--- /dev/null
+++ b/ntcore/src/generate/java/NetworkTableValue.java.jinja
@@ -0,0 +1,248 @@
+// 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.Objects;
+
+/** A network table entry value. */
+@SuppressWarnings({"UnnecessaryParentheses", "PMD.MethodReturnsInternalArray"})
+public final class NetworkTableValue {
+ NetworkTableValue(NetworkTableType type, Object value, long time, long serverTime) {
+ m_type = type;
+ m_value = value;
+ m_time = time;
+ m_serverTime = serverTime;
+ }
+
+ NetworkTableValue(NetworkTableType type, Object value, long time) {
+ this(type, value, time, time == 0 ? 0 : 1);
+ }
+
+ NetworkTableValue(NetworkTableType type, Object value) {
+ this(type, value, NetworkTablesJNI.now(), 1);
+ }
+
+ NetworkTableValue(int type, Object value, long time, long serverTime) {
+ this(NetworkTableType.getFromInt(type), value, time, serverTime);
+ }
+
+ /**
+ * Get the data type.
+ *
+ * @return The type.
+ */
+ public NetworkTableType getType() {
+ return m_type;
+ }
+
+ /**
+ * Get the data value stored.
+ *
+ * @return The type.
+ */
+ public Object getValue() {
+ return m_value;
+ }
+
+ /**
+ * Get the creation time of the value in local time.
+ *
+ * @return The time, in the units returned by NetworkTablesJNI.now().
+ */
+ public long getTime() {
+ return m_time;
+ }
+
+ /**
+ * Get the creation time of the value in server time.
+ *
+ * @return The server time.
+ */
+ public long getServerTime() {
+ return m_serverTime;
+ }
+
+ /*
+ * Type Checkers
+ */
+
+ /**
+ * Determine if entry value contains a value or is unassigned.
+ *
+ * @return True if the entry value contains a value.
+ */
+ public boolean isValid() {
+ return m_type != NetworkTableType.kUnassigned;
+ }
+{% for t in types %}
+ /**
+ * Determine if entry value contains a {{ t.java.ValueType }}.
+ *
+ * @return True if the entry value is of {{ t.java.ValueType }} type.
+ */
+ public boolean is{{ t.TypeName }}() {
+ return m_type == NetworkTableType.k{{ t.TypeName }};
+ }
+{% endfor %}
+ /*
+ * Type-Safe Getters
+ */
+{% for t in types %}
+ /**
+ * Get the {{ t.java.ValueType }} value.
+ *
+ * @return The {{ t.java.ValueType }} value.
+ * @throws ClassCastException if the entry value is not of {{ t.java.ValueType }} type.
+ */
+ public {{ t.java.ValueType }} get{{ t.TypeName }}() {
+ if (m_type != NetworkTableType.k{{ t.TypeName }}) {
+ throw new ClassCastException("cannot convert " + m_type + " to {{ t.java.ValueType }}");
+ }
+ return {{ t.java.FromStorageBegin }}m_value{{ t.java.FromStorageEnd }};
+ }
+{% endfor %}
+ /*
+ * Factory functions.
+ */
+{% for t in types %}
+ /**
+ * Creates a {{ t.java.ValueType }} value.
+ *
+ * @param value the value
+ * @return The entry value
+ */
+ public static NetworkTableValue make{{ t.TypeName }}({{ t.java.ValueType }} value) {
+ return new NetworkTableValue(NetworkTableType.k{{ t.TypeName }}, {{ t.java.ToWrapObject }}(value));
+ }
+
+ /**
+ * Creates a {{ t.java.ValueType }} value.
+ *
+ * @param value the value
+ * @param time the creation time to use (instead of the current time)
+ * @return The entry value
+ */
+ public static NetworkTableValue make{{ t.TypeName }}({{ t.java.ValueType }} value, long time) {
+ return new NetworkTableValue(NetworkTableType.k{{ t.TypeName }}, {{ t.java.ToWrapObject }}(value), time);
+ }
+{% if t.java.WrapValueType %}
+ /**
+ * Creates a {{ t.java.ValueType }} value.
+ *
+ * @param value the value
+ * @return The entry value
+ */
+ public static NetworkTableValue make{{ t.TypeName }}({{ t.java.WrapValueType }} value) {
+ return new NetworkTableValue(NetworkTableType.k{{ t.TypeName }}, toNative{{ t.TypeName }}(value));
+ }
+
+ /**
+ * Creates a {{ t.java.ValueType }} value.
+ *
+ * @param value the value
+ * @param time the creation time to use (instead of the current time)
+ * @return The entry value
+ */
+ public static NetworkTableValue make{{ t.TypeName }}({{ t.java.WrapValueType }} value, long time) {
+ return new NetworkTableValue(NetworkTableType.k{{ t.TypeName }}, toNative{{ t.TypeName }}(value), time);
+ }
+{% endif -%}
+{% endfor %}
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof NetworkTableValue)) {
+ return false;
+ }
+ NetworkTableValue ntOther = (NetworkTableValue) other;
+ return m_type == ntOther.m_type && m_value.equals(ntOther.m_value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(m_type, m_value);
+ }
+
+ // arraycopy() doesn't know how to unwrap boxed values; this is a false positive in PMD
+ // (see https://sourceforge.net/p/pmd/bugs/804/)
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static boolean[] toNativeBooleanArray(Boolean[] arr) {
+ boolean[] out = new boolean[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i];
+ }
+ return out;
+ }
+
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static double[] toNativeDoubleArray(Number[] arr) {
+ double[] out = new double[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i].doubleValue();
+ }
+ return out;
+ }
+
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static long[] toNativeIntegerArray(Number[] arr) {
+ long[] out = new long[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i].longValue();
+ }
+ return out;
+ }
+
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static float[] toNativeFloatArray(Number[] arr) {
+ float[] out = new float[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i].floatValue();
+ }
+ return out;
+ }
+
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static Boolean[] fromNativeBooleanArray(boolean[] arr) {
+ Boolean[] out = new Boolean[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i];
+ }
+ return out;
+ }
+
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static Long[] fromNativeIntegerArray(long[] arr) {
+ Long[] out = new Long[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i];
+ }
+ return out;
+ }
+
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static Float[] fromNativeFloatArray(float[] arr) {
+ Float[] out = new Float[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i];
+ }
+ return out;
+ }
+
+ @SuppressWarnings("PMD.AvoidArrayLoops")
+ static Double[] fromNativeDoubleArray(double[] arr) {
+ Double[] out = new Double[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ out[i] = arr[i];
+ }
+ return out;
+ }
+
+ private NetworkTableType m_type;
+ private Object m_value;
+ private long m_time;
+ private long m_serverTime;
+}
diff --git a/ntcore/src/generate/java/NetworkTablesJNI.java.jinja b/ntcore/src/generate/java/NetworkTablesJNI.java.jinja
new file mode 100644
index 0000000..2f119fe
--- /dev/null
+++ b/ntcore/src/generate/java/NetworkTablesJNI.java.jinja
@@ -0,0 +1,287 @@
+// 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.RuntimeLoader;
+import edu.wpi.first.util.datalog.DataLog;
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.OptionalLong;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public final class NetworkTablesJNI {
+ static boolean libraryLoaded = false;
+ static RuntimeLoader<NetworkTablesJNI> loader = null;
+
+ public static class Helper {
+ private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
+
+ public static boolean getExtractOnStaticLoad() {
+ return extractOnStaticLoad.get();
+ }
+
+ public static void setExtractOnStaticLoad(boolean load) {
+ extractOnStaticLoad.set(load);
+ }
+ }
+
+ static {
+ if (Helper.getExtractOnStaticLoad()) {
+ try {
+ loader =
+ new RuntimeLoader<>(
+ "ntcorejni", RuntimeLoader.getDefaultExtractionRoot(), NetworkTablesJNI.class);
+ loader.loadLibrary();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ System.exit(1);
+ }
+ libraryLoaded = true;
+ }
+ }
+
+ /**
+ * Force load the library.
+ *
+ * @throws IOException if the library fails to load
+ */
+ public static synchronized void forceLoad() throws IOException {
+ if (libraryLoaded) {
+ return;
+ }
+ loader =
+ new RuntimeLoader<>(
+ "ntcorejni", RuntimeLoader.getDefaultExtractionRoot(), NetworkTablesJNI.class);
+ loader.loadLibrary();
+ libraryLoaded = true;
+ }
+
+ private static PubSubOptions buildOptions(PubSubOption... options) {
+ if (options.length == 0) {
+ return null; // optimize common case (JNI checks for null)
+ }
+ return new PubSubOptions(options);
+ }
+
+ public static native int getDefaultInstance();
+
+ public static native int createInstance();
+
+ public static native void destroyInstance(int inst);
+
+ public static native int getInstanceFromHandle(int handle);
+
+ private static native int getEntryImpl(
+ int topic, int type, String typeStr, PubSubOptions options);
+
+ public static native int getEntry(int inst, String key);
+
+ public static int getEntry(
+ int topic, int type, String typeStr, PubSubOptions options) {
+ return getEntryImpl(topic, type, typeStr, options);
+ }
+
+ public static int getEntry(
+ int topic, int type, String typeStr, PubSubOption... options) {
+ return getEntryImpl(topic, type, typeStr, buildOptions(options));
+ }
+
+ public static native String getEntryName(int entry);
+
+ public static native long getEntryLastChange(int entry);
+
+ public static native int getType(int entry);
+
+ /* Topic functions */
+
+ public static native int[] getTopics(int inst, String prefix, int types);
+
+ public static native int[] getTopicsStr(int inst, String prefix, String[] types);
+
+ public static native TopicInfo[] getTopicInfos(
+ NetworkTableInstance instObject, int inst, String prefix, int types);
+
+ public static native TopicInfo[] getTopicInfosStr(
+ NetworkTableInstance instObject, int inst, String prefix, String[] types);
+
+ public static native int getTopic(int inst, String name);
+
+ public static native String getTopicName(int topic);
+
+ public static native int getTopicType(int topic);
+
+ public static native void setTopicPersistent(int topic, boolean value);
+
+ public static native boolean getTopicPersistent(int topic);
+
+ public static native void setTopicRetained(int topic, boolean value);
+
+ public static native boolean getTopicRetained(int topic);
+
+ public static native String getTopicTypeString(int topic);
+
+ public static native boolean getTopicExists(int topic);
+
+ public static native String getTopicProperty(int topic, String name);
+
+ public static native void setTopicProperty(int topic, String name, String value);
+
+ public static native void deleteTopicProperty(int topic, String name);
+
+ public static native String getTopicProperties(int topic);
+
+ public static native void setTopicProperties(int topic, String properties);
+
+ public static native int subscribe(
+ int topic, int type, String typeStr, PubSubOptions options);
+
+ public static int subscribe(
+ int topic, int type, String typeStr, PubSubOption... options) {
+ return subscribe(topic, type, typeStr, buildOptions(options));
+ }
+
+ public static native void unsubscribe(int sub);
+
+ public static native int publish(
+ int topic, int type, String typeStr, PubSubOptions options);
+
+ public static int publish(
+ int topic, int type, String typeStr, PubSubOption... options) {
+ return publish(topic, type, typeStr, buildOptions(options));
+ }
+
+ public static native int publishEx(
+ int topic, int type, String typeStr, String properties, PubSubOptions options);
+
+ public static int publishEx(
+ int topic, int type, String typeStr, String properties, PubSubOption... options) {
+ return publishEx(topic, type, typeStr, properties, buildOptions(options));
+ }
+
+ public static native void unpublish(int pubentry);
+
+ public static native void releaseEntry(int entry);
+
+ public static native void release(int pubsubentry);
+
+ public static native int getTopicFromHandle(int pubsubentry);
+
+ public static native int subscribeMultiple(int inst, String[] prefixes, PubSubOptions options);
+
+ public static int subscribeMultiple(int inst, String[] prefixes, PubSubOption... options) {
+ return subscribeMultiple(inst, prefixes, buildOptions(options));
+ }
+
+ public static native void unsubscribeMultiple(int sub);
+{% for t in types %}
+ public static native Timestamped{{ t.TypeName }} getAtomic{{ t.TypeName }}(
+ int subentry, {{ t.java.ValueType }} defaultValue);
+
+ public static native Timestamped{{ t.TypeName }}[] readQueue{{ t.TypeName }}(int subentry);
+
+ public static native {{ t.java.ValueType }}[] readQueueValues{{ t.TypeName }}(int subentry);
+
+ public static native boolean set{{ t.TypeName }}(int entry, long time, {{ t.java.ValueType }} value);
+
+ public static native {{ t.java.ValueType }} get{{ t.TypeName }}(int entry, {{ t.java.ValueType }} defaultValue);
+
+ public static native boolean setDefault{{ t.TypeName }}(int entry, long time, {{ t.java.ValueType }} defaultValue);
+{% endfor %}
+ public static native NetworkTableValue[] readQueueValue(int subentry);
+
+ public static native NetworkTableValue getValue(int entry);
+
+ public static native void setEntryFlags(int entry, int flags);
+
+ public static native int getEntryFlags(int entry);
+
+ public static native TopicInfo getTopicInfo(NetworkTableInstance inst, int topic);
+
+ public static native int createListenerPoller(int inst);
+
+ public static native void destroyListenerPoller(int poller);
+
+ private static int kindsToMask(EnumSet<NetworkTableEvent.Kind> kinds) {
+ int mask = 0;
+ for (NetworkTableEvent.Kind kind : kinds) {
+ mask |= kind.getValue();
+ }
+ return mask;
+ }
+
+ public static int addListener(int poller, String[] prefixes, EnumSet<NetworkTableEvent.Kind> kinds) {
+ return addListener(poller, prefixes, kindsToMask(kinds));
+ }
+
+ public static int addListener(int poller, int handle, EnumSet<NetworkTableEvent.Kind> kinds) {
+ return addListener(poller, handle, kindsToMask(kinds));
+ }
+
+ public static native int addListener(int poller, String[] prefixes, int mask);
+
+ public static native int addListener(int poller, int handle, int mask);
+
+ public static native NetworkTableEvent[] readListenerQueue(
+ NetworkTableInstance inst, int poller);
+
+ public static native void removeListener(int listener);
+
+ public static native int getNetworkMode(int inst);
+
+ public static native void startLocal(int inst);
+
+ public static native void stopLocal(int inst);
+
+ public static native void startServer(
+ int inst, String persistFilename, String listenAddress, int port3, int port4);
+
+ public static native void stopServer(int inst);
+
+ public static native void startClient3(int inst, String identity);
+
+ public static native void startClient4(int inst, String identity);
+
+ public static native void stopClient(int inst);
+
+ public static native void setServer(int inst, String serverName, int port);
+
+ public static native void setServer(int inst, String[] serverNames, int[] ports);
+
+ public static native void setServerTeam(int inst, int team, int port);
+
+ public static native void startDSClient(int inst, int port);
+
+ public static native void stopDSClient(int inst);
+
+ public static native void flushLocal(int inst);
+
+ public static native void flush(int inst);
+
+ public static native ConnectionInfo[] getConnections(int inst);
+
+ public static native boolean isConnected(int inst);
+
+ public static native OptionalLong getServerTimeOffset(int inst);
+
+ public static native long now();
+
+ private static native int startEntryDataLog(int inst, long log, String prefix, String logPrefix);
+
+ public static int startEntryDataLog(int inst, DataLog log, String prefix, String logPrefix) {
+ return startEntryDataLog(inst, log.getImpl(), prefix, logPrefix);
+ }
+
+ public static native void stopEntryDataLog(int logger);
+
+ private static native int startConnectionDataLog(int inst, long log, String name);
+
+ public static int startConnectionDataLog(int inst, DataLog log, String name) {
+ return startConnectionDataLog(inst, log.getImpl(), name);
+ }
+
+ public static native void stopConnectionDataLog(int logger);
+
+ public static native int addLogger(int poller, int minLevel, int maxLevel);
+}
diff --git a/ntcore/src/generate/java/Publisher.java.jinja b/ntcore/src/generate/java/Publisher.java.jinja
new file mode 100644
index 0000000..19ead36
--- /dev/null
+++ b/ntcore/src/generate/java/Publisher.java.jinja
@@ -0,0 +1,49 @@
+// 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.ConsumerFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Consumer;
+
+/** NetworkTables {{ TypeName }} publisher. */
+public interface {{ TypeName }}Publisher extends Publisher, {{ java.FunctionTypePrefix }}Consumer{{ java.FunctionTypeSuffix }} {
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ @Override
+ {{ TypeName }}Topic getTopic();
+
+ /**
+ * Publish a new value using current NT time.
+ *
+ * @param value value to publish
+ */
+ default void set({{ java.ValueType }} 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({{ java.ValueType }} 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({{ java.ValueType }} value);
+
+ @Override
+ default void accept({{ java.ValueType }} value) {
+ set(value);
+ }
+}
diff --git a/ntcore/src/generate/java/Subscriber.java.jinja b/ntcore/src/generate/java/Subscriber.java.jinja
new file mode 100644
index 0000000..0ea09a3
--- /dev/null
+++ b/ntcore/src/generate/java/Subscriber.java.jinja
@@ -0,0 +1,83 @@
+// 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.SupplierFunctionPackage|default('java.util.function') }}.{{ java.FunctionTypePrefix }}Supplier;
+
+/** NetworkTables {{ TypeName }} subscriber. */
+@SuppressWarnings("PMD.MissingOverride")
+public interface {{ TypeName }}Subscriber extends Subscriber, {{ java.FunctionTypePrefix }}Supplier{{ java.FunctionTypeSuffix }} {
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ @Override
+ {{ TypeName }}Topic getTopic();
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the stored default value.
+ *
+ * @return value
+ */
+ {{ java.ValueType }} get();
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+ {{ java.ValueType }} get({{ java.ValueType }} defaultValue);
+{% if java.FunctionTypePrefix %}
+ @Override
+ default {{ java.ValueType }} getAs{{ java.FunctionTypePrefix }}() {
+ return get();
+ }
+{% endif %}
+ /**
+ * Get the last published value along with its timestamp
+ * If no value has been published, returns the stored default value and a
+ * timestamp of 0.
+ *
+ * @return timestamped value
+ */
+ Timestamped{{ TypeName }} getAtomic();
+
+ /**
+ * Get the last published value along with its timestamp
+ * If no value has been published, returns the passed defaultValue and a
+ * timestamp of 0.
+ *
+ * @param defaultValue default value to return if no value has been published
+ * @return timestamped value
+ */
+ Timestamped{{ TypeName }} getAtomic({{ java.ValueType }} defaultValue);
+
+ /**
+ * Get an array of all value changes since the last call to readQueue.
+ * Also provides a timestamp for each value.
+ *
+ * <p>The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @return Array of timestamped values; empty array if no new changes have
+ * been published since the previous call.
+ */
+ Timestamped{{ TypeName }}[] readQueue();
+
+ /**
+ * Get an array of all value changes since the last call to readQueue.
+ *
+ * <p>The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @return Array of values; empty array if no new changes have been
+ * published since the previous call.
+ */
+ {{ java.ValueType }}[] readQueueValues();
+}
diff --git a/ntcore/src/generate/java/Timestamped.java.jinja b/ntcore/src/generate/java/Timestamped.java.jinja
new file mode 100644
index 0000000..288af81
--- /dev/null
+++ b/ntcore/src/generate/java/Timestamped.java.jinja
@@ -0,0 +1,40 @@
+// 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 {{ TypeName }}. */
+@SuppressWarnings("PMD.ArrayIsStoredDirectly")
+public final class Timestamped{{ TypeName }} {
+ /**
+ * Create a timestamped value.
+ *
+ * @param timestamp timestamp in local time base
+ * @param serverTime timestamp in server time base
+ * @param value value
+ */
+ public Timestamped{{ TypeName }}(long timestamp, long serverTime, {{ java.ValueType }} 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 {{ java.ValueType }} value;
+}
diff --git a/ntcore/src/generate/java/Topic.java.jinja b/ntcore/src/generate/java/Topic.java.jinja
new file mode 100644
index 0000000..e22fa3b
--- /dev/null
+++ b/ntcore/src/generate/java/Topic.java.jinja
@@ -0,0 +1,223 @@
+// 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 {{ TypeName }} topic. */
+public final class {{ TypeName }}Topic extends Topic {
+{%- if TypeString %}
+ /** The default type string for this topic type. */
+ public static final String kTypeString = {{ TypeString }};
+{% endif %}
+ /**
+ * Construct from a generic topic.
+ *
+ * @param topic Topic
+ */
+ public {{ TypeName }}Topic(Topic topic) {
+ super(topic.m_inst, topic.m_handle);
+ }
+
+ /**
+ * Constructor; use NetworkTableInstance.get{{TypeName}}Topic() instead.
+ *
+ * @param inst Instance
+ * @param handle Native handle
+ */
+ public {{ TypeName }}Topic(NetworkTableInstance inst, int handle) {
+ super(inst, handle);
+ }
+
+ /**
+ * 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.
+ *
+{%- if not TypeString %}
+ * @param typeString type string
+{% endif %}
+ * @param defaultValue default value used when a default is not provided to a
+ * getter function
+ * @param options subscribe options
+ * @return subscriber
+ */
+ public {{ TypeName }}Subscriber subscribe(
+{%- if not TypeString %}
+ String typeString,
+{% endif %}
+ {{ java.ValueType }} defaultValue,
+ PubSubOption... options) {
+ return new {{ TypeName }}EntryImpl(
+ this,
+ NetworkTablesJNI.subscribe(
+ m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+ {{ TypeString|default('typeString') }}, options),
+ defaultValue);
+ }
+{% if TypeString %}
+ /**
+ * Create a new subscriber to the topic, with specified type string.
+ *
+ * <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 typeString type string
+ * @param defaultValue default value used when a default is not provided to a
+ * getter function
+ * @param options subscribe options
+ * @return subscriber
+ */
+ public {{ TypeName }}Subscriber subscribeEx(
+ String typeString,
+ {{ java.ValueType }} defaultValue,
+ PubSubOption... options) {
+ return new {{ TypeName }}EntryImpl(
+ this,
+ NetworkTablesJNI.subscribe(
+ m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+ typeString, options),
+ defaultValue);
+ }
+{% endif %}
+ /**
+ * 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.
+ *
+{%- if not TypeString %}
+ * @param typeString type string
+{% endif %}
+ * @param options publish options
+ * @return publisher
+ */
+ public {{ TypeName }}Publisher publish(
+{%- if not TypeString %}
+ String typeString,
+{% endif %}
+ PubSubOption... options) {
+ return new {{ TypeName }}EntryImpl(
+ this,
+ NetworkTablesJNI.publish(
+ m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+ {{ TypeString|default('typeString') }}, options),
+ {{ java.EmptyValue }});
+ }
+
+ /**
+ * 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 typeString type string
+ * @param properties JSON properties
+ * @param options publish options
+ * @return publisher
+ * @throws IllegalArgumentException if properties is not a JSON object
+ */
+ public {{ TypeName }}Publisher publishEx(
+ String typeString,
+ String properties,
+ PubSubOption... options) {
+ return new {{ TypeName }}EntryImpl(
+ this,
+ NetworkTablesJNI.publishEx(
+ m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+ typeString, properties, options),
+ {{ java.EmptyValue }});
+ }
+
+ /**
+ * 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.
+ *
+{%- if not TypeString %}
+ * @param typeString type string
+{% endif %}
+ * @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 {{ TypeName }}Entry getEntry(
+{%- if not TypeString %}
+ String typeString,
+{% endif %}
+ {{ java.ValueType }} defaultValue,
+ PubSubOption... options) {
+ return new {{ TypeName }}EntryImpl(
+ this,
+ NetworkTablesJNI.getEntry(
+ m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+ {{ TypeString|default('typeString') }}, options),
+ defaultValue);
+ }
+{% if TypeString %}
+ /**
+ * Create a new entry for the topic, with specified type string.
+ *
+ * <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 typeString type string
+ * @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 {{ TypeName }}Entry getEntryEx(
+ String typeString,
+ {{ java.ValueType }} defaultValue,
+ PubSubOption... options) {
+ return new {{ TypeName }}EntryImpl(
+ this,
+ NetworkTablesJNI.getEntry(
+ m_handle, NetworkTableType.k{{ TypeName }}.getValue(),
+ typeString, options),
+ defaultValue);
+ }
+{% endif %}
+}
diff --git a/ntcore/src/generate/types.json b/ntcore/src/generate/types.json
new file mode 100644
index 0000000..31a71b3
--- /dev/null
+++ b/ntcore/src/generate/types.json
@@ -0,0 +1,366 @@
+[
+ {
+ "TypeName": "Boolean",
+ "TypeString": "\"boolean\"",
+ "c": {
+ "ValueType": "NT_Bool",
+ "ParamType": "NT_Bool"
+ },
+ "cpp": {
+ "ValueType": "bool",
+ "ParamType": "bool",
+ "TYPE_NAME": "BOOLEAN"
+ },
+ "java": {
+ "ValueType": "boolean",
+ "EmptyValue": "false",
+ "ConsumerFunctionPackage": "edu.wpi.first.util.function",
+ "FunctionTypePrefix": "Boolean",
+ "ToWrapObject": "Boolean.valueOf",
+ "FromStorageBegin": "(Boolean) "
+ },
+ "jni": {
+ "jtype": "jboolean",
+ "jtypestr": "Z",
+ "JavaObject": false,
+ "FromJavaBegin": "",
+ "FromJavaEnd": " != JNI_FALSE",
+ "ToJavaBegin": "static_cast<jboolean>(",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJBooleanArray"
+ }
+ },
+ {
+ "TypeName": "Integer",
+ "TypeString": "\"int\"",
+ "c": {
+ "ValueType": "int64_t",
+ "ParamType": "int64_t"
+ },
+ "cpp": {
+ "ValueType": "int64_t",
+ "ParamType": "int64_t",
+ "TYPE_NAME": "INTEGER"
+ },
+ "java": {
+ "ValueType": "long",
+ "EmptyValue": "0",
+ "FunctionTypePrefix": "Long",
+ "ToWrapObject": "Long.valueOf",
+ "FromStorageBegin": "((Number) ",
+ "FromStorageEnd": ").longValue()"
+ },
+ "jni": {
+ "jtype": "jlong",
+ "jtypestr": "J",
+ "JavaObject": false,
+ "FromJavaBegin": "",
+ "FromJavaEnd": "",
+ "ToJavaBegin": "static_cast<jlong>(",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJLongArray"
+ }
+ },
+ {
+ "TypeName": "Float",
+ "TypeString": "\"float\"",
+ "c": {
+ "ValueType": "float",
+ "ParamType": "float"
+ },
+ "cpp": {
+ "ValueType": "float",
+ "ParamType": "float",
+ "TYPE_NAME": "FLOAT"
+ },
+ "java": {
+ "ValueType": "float",
+ "EmptyValue": "0",
+ "ConsumerFunctionPackage": "edu.wpi.first.util.function",
+ "SupplierFunctionPackage": "edu.wpi.first.util.function",
+ "FunctionTypePrefix": "Float",
+ "ToWrapObject": "Float.valueOf",
+ "FromStorageBegin": "((Number) ",
+ "FromStorageEnd": ").floatValue()"
+ },
+ "jni": {
+ "jtype": "jfloat",
+ "jtypestr": "F",
+ "JavaObject": false,
+ "FromJavaBegin": "",
+ "FromJavaEnd": "",
+ "ToJavaBegin": "static_cast<jfloat>(",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJFloatArray"
+ }
+ },
+ {
+ "TypeName": "Double",
+ "TypeString": "\"double\"",
+ "c": {
+ "ValueType": "double",
+ "ParamType": "double"
+ },
+ "cpp": {
+ "ValueType": "double",
+ "ParamType": "double",
+ "TYPE_NAME": "DOUBLE"
+ },
+ "java": {
+ "ValueType": "double",
+ "EmptyValue": "0",
+ "FunctionTypePrefix": "Double",
+ "ToWrapObject": "Double.valueOf",
+ "FromStorageBegin": "((Number) ",
+ "FromStorageEnd": ").doubleValue()"
+ },
+ "jni": {
+ "jtype": "jdouble",
+ "jtypestr": "D",
+ "JavaObject": false,
+ "FromJavaBegin": "",
+ "FromJavaEnd": "",
+ "ToJavaBegin": "static_cast<jdouble>(",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJDoubleArray"
+ }
+ },
+ {
+ "TypeName": "String",
+ "TypeString": "\"string\"",
+ "c": {
+ "ValueType": "char*",
+ "ParamType": "const char*",
+ "IsArray": true
+ },
+ "cpp": {
+ "ValueType": "std::string",
+ "ParamType": "std::string_view",
+ "TYPE_NAME": "STRING",
+ "INCLUDES": "#include <string>\n#include <string_view>\n#include <utility>",
+ "SmallRetType": "std::string_view",
+ "SmallElemType": "char"
+ },
+ "java": {
+ "ValueType": "String",
+ "EmptyValue": "\"\"",
+ "FunctionTypeSuffix": "<String>",
+ "FromStorageBegin": "(String) "
+ },
+ "jni": {
+ "jtype": "jstring",
+ "jtypestr": "Ljava/lang/String;",
+ "JavaObject": true,
+ "FromJavaBegin": "JStringRef{env, ",
+ "FromJavaEnd": "}",
+ "ToJavaBegin": "MakeJString(env, ",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJStringArray"
+ }
+ },
+ {
+ "TypeName": "Raw",
+ "c": {
+ "ValueType": "uint8_t*",
+ "ParamType": "const uint8_t*",
+ "IsArray": true
+ },
+ "cpp": {
+ "ValueType": "std::vector<uint8_t>",
+ "ParamType": "std::span<const uint8_t>",
+ "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()",
+ "TYPE_NAME": "RAW",
+ "INCLUDES": "#include <utility>",
+ "SmallRetType": "std::span<uint8_t>",
+ "SmallElemType": "uint8_t"
+ },
+ "java": {
+ "ValueType": "byte[]",
+ "EmptyValue": "new byte[] {}",
+ "FunctionTypeSuffix": "<byte[]>",
+ "FromStorageBegin": "(byte[]) "
+ },
+ "jni": {
+ "jtype": "jbyteArray",
+ "jtypestr": "[B",
+ "JavaObject": true,
+ "FromJavaBegin": "CriticalJByteArrayRef{env, ",
+ "FromJavaEnd": "}.uarray()",
+ "ToJavaBegin": "MakeJByteArray(env, ",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJObjectArray"
+ }
+ },
+ {
+ "TypeName": "BooleanArray",
+ "TypeString": "\"boolean[]\"",
+ "c": {
+ "ValueType": "NT_Bool*",
+ "ParamType": "const NT_Bool*",
+ "IsArray": true
+ },
+ "cpp": {
+ "ValueType": "std::vector<int>",
+ "ParamType": "std::span<const int>",
+ "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()",
+ "TYPE_NAME": "BOOLEAN_ARRAY",
+ "INCLUDES": "#include <utility>",
+ "SmallRetType": "std::span<int>",
+ "SmallElemType": "int"
+ },
+ "java": {
+ "ValueType": "boolean[]",
+ "WrapValueType": "Boolean[]",
+ "EmptyValue": "new boolean[] {}",
+ "FunctionTypeSuffix": "<boolean[]>",
+ "FromStorageBegin": "(boolean[]) "
+ },
+ "jni": {
+ "jtype": "jbooleanArray",
+ "jtypestr": "[Z",
+ "JavaObject": true,
+ "FromJavaBegin": "FromJavaBooleanArray(env, ",
+ "FromJavaEnd": ")",
+ "ToJavaBegin": "MakeJBooleanArray(env, ",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJObjectArray"
+ }
+ },
+ {
+ "TypeName": "IntegerArray",
+ "TypeString": "\"int[]\"",
+ "c": {
+ "ValueType": "int64_t*",
+ "ParamType": "const int64_t*",
+ "IsArray": true
+ },
+ "cpp": {
+ "ValueType": "std::vector<int64_t>",
+ "ParamType": "std::span<const int64_t>",
+ "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()",
+ "TYPE_NAME": "INTEGER_ARRAY",
+ "INCLUDES": "#include <utility>",
+ "SmallRetType": "std::span<int64_t>",
+ "SmallElemType": "int64_t"
+ },
+ "java": {
+ "ValueType": "long[]",
+ "WrapValueType": "Long[]",
+ "EmptyValue": "new long[] {}",
+ "FunctionTypeSuffix": "<long[]>",
+ "FromStorageBegin": "(long[]) "
+ },
+ "jni": {
+ "jtype": "jlongArray",
+ "jtypestr": "[J",
+ "JavaObject": true,
+ "FromJavaBegin": "CriticalJLongArrayRef{env, ",
+ "FromJavaEnd": "}",
+ "ToJavaBegin": "MakeJLongArray(env, ",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJObjectArray"
+ }
+ },
+ {
+ "TypeName": "FloatArray",
+ "TypeString": "\"float[]\"",
+ "c": {
+ "ValueType": "float*",
+ "ParamType": "const float*",
+ "IsArray": true
+ },
+ "cpp": {
+ "ValueType": "std::vector<float>",
+ "ParamType": "std::span<const float>",
+ "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()",
+ "TYPE_NAME": "FLOAT_ARRAY",
+ "INCLUDES": "#include <utility>",
+ "SmallRetType": "std::span<float>",
+ "SmallElemType": "float"
+ },
+ "java": {
+ "ValueType": "float[]",
+ "WrapValueType": "Float[]",
+ "EmptyValue": "new float[] {}",
+ "FunctionTypeSuffix": "<float[]>",
+ "FromStorageBegin": "(float[]) "
+ },
+ "jni": {
+ "jtype": "jfloatArray",
+ "jtypestr": "[F",
+ "JavaObject": true,
+ "FromJavaBegin": "CriticalJFloatArrayRef{env, ",
+ "FromJavaEnd": "}",
+ "ToJavaBegin": "MakeJFloatArray(env, ",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJObjectArray"
+ }
+ },
+ {
+ "TypeName": "DoubleArray",
+ "TypeString": "\"double[]\"",
+ "c": {
+ "ValueType": "double*",
+ "ParamType": "const double*",
+ "IsArray": true
+ },
+ "cpp": {
+ "ValueType": "std::vector<double>",
+ "ParamType": "std::span<const double>",
+ "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()",
+ "TYPE_NAME": "DOUBLE_ARRAY",
+ "INCLUDES": "#include <utility>",
+ "SmallRetType": "std::span<double>",
+ "SmallElemType": "double"
+ },
+ "java": {
+ "ValueType": "double[]",
+ "WrapValueType": "Double[]",
+ "EmptyValue": "new double[] {}",
+ "FunctionTypeSuffix": "<double[]>",
+ "FromStorageBegin": "(double[]) "
+ },
+ "jni": {
+ "jtype": "jdoubleArray",
+ "jtypestr": "[D",
+ "JavaObject": true,
+ "FromJavaBegin": "CriticalJDoubleArrayRef{env, ",
+ "FromJavaEnd": "}",
+ "ToJavaBegin": "MakeJDoubleArray(env, ",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJObjectArray"
+ }
+ },
+ {
+ "TypeName": "StringArray",
+ "TypeString": "\"string[]\"",
+ "c": {
+ "ValueType": "struct NT_String*",
+ "ParamType": "const struct NT_String*",
+ "IsArray": true
+ },
+ "cpp": {
+ "ValueType": "std::vector<std::string>",
+ "ParamType": "std::span<const std::string>",
+ "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()",
+ "TYPE_NAME": "STRING_ARRAY",
+ "INCLUDES": "#include <utility>"
+ },
+ "java": {
+ "ValueType": "String[]",
+ "EmptyValue": "new String[] {}",
+ "FunctionTypeSuffix": "<String[]>",
+ "FromStorageBegin": "(String[]) "
+ },
+ "jni": {
+ "jtype": "jobjectArray",
+ "jtypestr": "[Ljava/lang/Object;",
+ "JavaObject": true,
+ "FromJavaBegin": "FromJavaStringArray(env, ",
+ "FromJavaEnd": ")",
+ "ToJavaBegin": "MakeJStringArray(env, ",
+ "ToJavaEnd": ")",
+ "ToJavaArray": "MakeJObjectArray"
+ }
+ }
+]
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionInfo.java b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionInfo.java
index 477d53b..147b2ea 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionInfo.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionInfo.java
@@ -5,34 +5,30 @@
package edu.wpi.first.networktables;
/** NetworkTables Connection information. */
+@SuppressWarnings("MemberName")
public final class ConnectionInfo {
/**
* The remote identifier (as set on the remote node by {@link
- * NetworkTableInstance#setNetworkIdentity(String)}).
+ * NetworkTableInstance#startClient4(String)}).
*/
- @SuppressWarnings("MemberName")
public final String remote_id;
/** The IP address of the remote node. */
- @SuppressWarnings("MemberName")
public final String remote_ip;
/** The port number of the remote node. */
- @SuppressWarnings("MemberName")
public final int remote_port;
/**
* The last time any update was received from the remote node (same scale as returned by {@link
* NetworkTablesJNI#now()}).
*/
- @SuppressWarnings("MemberName")
public final long last_update;
/**
* The protocol version being used for this connection. This is in protocol layer format, so
* 0x0200 = 2.0, 0x0300 = 3.0).
*/
- @SuppressWarnings("MemberName")
public final int protocol_version;
/**
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java
deleted file mode 100644
index 544e075..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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 Connection notification. */
-public final class ConnectionNotification {
- /** Listener that was triggered. */
- @SuppressWarnings("MemberName")
- public final int listener;
-
- /** True if event is due to connection being established. */
- @SuppressWarnings("MemberName")
- public final boolean connected;
-
- /** Connection information. */
- @SuppressWarnings("MemberName")
- public final ConnectionInfo conn;
-
- /**
- * Constructor. This should generally only be used internally to NetworkTables.
- *
- * @param inst Instance
- * @param listener Listener that was triggered
- * @param connected Connected if true
- * @param conn Connection information
- */
- public ConnectionNotification(
- NetworkTableInstance inst, int listener, boolean connected, ConnectionInfo conn) {
- this.m_inst = inst;
- this.listener = listener;
- this.connected = connected;
- this.conn = conn;
- }
-
- private final NetworkTableInstance m_inst;
-
- public NetworkTableInstance getInstance() {
- return m_inst;
- }
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/EntryBase.java b/ntcore/src/main/java/edu/wpi/first/networktables/EntryBase.java
new file mode 100644
index 0000000..48d4a9c
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/EntryBase.java
@@ -0,0 +1,44 @@
+// 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 entry base implementation. */
+public abstract class EntryBase implements Subscriber, Publisher {
+ /**
+ * Constructor.
+ *
+ * @param handle handle
+ */
+ public EntryBase(int handle) {
+ m_handle = handle;
+ }
+
+ @Override
+ public boolean isValid() {
+ return m_handle != 0;
+ }
+
+ @Override
+ public int getHandle() {
+ return m_handle;
+ }
+
+ @Override
+ public void close() {
+ NetworkTablesJNI.release(m_handle);
+ }
+
+ @Override
+ public boolean exists() {
+ return NetworkTablesJNI.getTopicExists(m_handle);
+ }
+
+ @Override
+ public long getLastChange() {
+ return NetworkTablesJNI.getEntryLastChange(m_handle);
+ }
+
+ protected int m_handle;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java b/ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java
deleted file mode 100644
index 471edc8..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// 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 Entry information. */
-public final class EntryInfo {
- /** Entry handle. */
- @SuppressWarnings("MemberName")
- public final int entry;
-
- /** Entry name. */
- @SuppressWarnings("MemberName")
- public final String name;
-
- /** Entry type. */
- @SuppressWarnings("MemberName")
- public final NetworkTableType type;
-
- /** Entry flags. */
- @SuppressWarnings("MemberName")
- public final int flags;
-
- /** Timestamp of last change to entry (type or value). */
- @SuppressWarnings("MemberName")
- public final long last_change;
-
- /**
- * Constructor. This should generally only be used internally to NetworkTables.
- *
- * @param inst Instance
- * @param entry Entry handle
- * @param name Name
- * @param type Type (integer version of {@link NetworkTableType})
- * @param flags Flags
- * @param lastChange Timestamp of last change
- */
- public EntryInfo(
- NetworkTableInstance inst, int entry, String name, int type, int flags, long lastChange) {
- this.m_inst = inst;
- this.entry = entry;
- this.name = name;
- this.type = NetworkTableType.getFromInt(type);
- this.flags = flags;
- this.last_change = lastChange;
- }
-
- /* Network table instance. */
- private final NetworkTableInstance m_inst;
-
- /* Cached entry object. */
- private NetworkTableEntry m_entryObject;
-
- /**
- * Get the entry as an object.
- *
- * @return NetworkTableEntry for this entry.
- */
- NetworkTableEntry getEntry() {
- if (m_entryObject == null) {
- m_entryObject = new NetworkTableEntry(m_inst, entry);
- }
- return m_entryObject;
- }
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/EntryListenerFlags.java b/ntcore/src/main/java/edu/wpi/first/networktables/EntryListenerFlags.java
deleted file mode 100644
index 856c962..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/EntryListenerFlags.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// 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;
-
-/**
- * Flag values for use with entry listeners.
- *
- * <p>The flags are a bitmask and must be OR'ed together to indicate the combination of events
- * desired to be received.
- *
- * <p>The constants kNew, kDelete, kUpdate, and kFlags represent different events that can occur to
- * entries.
- *
- * <p>By default, notifications are only generated for remote changes occurring after the listener
- * is created. The constants kImmediate and kLocal are modifiers that cause notifications to be
- * generated at other times.
- */
-public interface EntryListenerFlags {
- /**
- * Initial listener addition.
- *
- * <p>Set this flag to receive immediate notification of entries matching the flag criteria
- * (generally only useful when combined with kNew).
- */
- int kImmediate = 0x01;
-
- /**
- * Changed locally.
- *
- * <p>Set this flag to receive notification of both local changes and changes coming from remote
- * nodes. By default, notifications are only generated for remote changes. Must be combined with
- * some combination of kNew, kDelete, kUpdate, and kFlags to receive notifications of those
- * respective events.
- */
- int kLocal = 0x02;
-
- /**
- * Newly created entry.
- *
- * <p>Set this flag to receive a notification when an entry is created.
- */
- int kNew = 0x04;
-
- /**
- * Entry was deleted.
- *
- * <p>Set this flag to receive a notification when an entry is deleted.
- */
- int kDelete = 0x08;
-
- /**
- * Entry's value changed.
- *
- * <p>Set this flag to receive a notification when an entry's value (or type) changes.
- */
- int kUpdate = 0x10;
-
- /**
- * Entry's flags changed.
- *
- * <p>Set this flag to receive a notification when an entry's flags value changes.
- */
- int kFlags = 0x20;
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java b/ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java
deleted file mode 100644
index 0f5cb51..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java
+++ /dev/null
@@ -1,73 +0,0 @@
-// 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 Entry notification. */
-public final class EntryNotification {
- /** Listener that was triggered. */
- @SuppressWarnings("MemberName")
- public final int listener;
-
- /** Entry handle. */
- @SuppressWarnings("MemberName")
- public final int entry;
-
- /** Entry name. */
- @SuppressWarnings("MemberName")
- public final String name;
-
- /** The new value. */
- @SuppressWarnings("MemberName")
- public final NetworkTableValue value;
-
- /**
- * Update flags. For example, {@link EntryListenerFlags#kNew} if the key did not previously exist.
- */
- @SuppressWarnings("MemberName")
- public final int flags;
-
- /**
- * Constructor. This should generally only be used internally to NetworkTables.
- *
- * @param inst Instance
- * @param listener Listener that was triggered
- * @param entry Entry handle
- * @param name Entry name
- * @param value The new value
- * @param flags Update flags
- */
- public EntryNotification(
- NetworkTableInstance inst,
- int listener,
- int entry,
- String name,
- NetworkTableValue value,
- int flags) {
- this.m_inst = inst;
- this.listener = listener;
- this.entry = entry;
- this.name = name;
- this.value = value;
- this.flags = flags;
- }
-
- /* Network table instance. */
- private final NetworkTableInstance m_inst;
-
- /* Cached entry object. */
- NetworkTableEntry m_entryObject;
-
- /**
- * Get the entry as an object.
- *
- * @return NetworkTableEntry for this entry.
- */
- public NetworkTableEntry getEntry() {
- if (m_entryObject == null) {
- m_entryObject = new NetworkTableEntry(m_inst, entry);
- }
- return m_entryObject;
- }
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/GenericEntry.java b/ntcore/src/main/java/edu/wpi/first/networktables/GenericEntry.java
new file mode 100644
index 0000000..77e7502
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/GenericEntry.java
@@ -0,0 +1,15 @@
+// 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 generic entry.
+ *
+ * <p>Unlike NetworkTableEntry, the entry goes away when close() is called.
+ */
+public interface GenericEntry extends GenericSubscriber, GenericPublisher {
+ /** Stops publishing the entry if it's published. */
+ void unpublish();
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/LogMessage.java b/ntcore/src/main/java/edu/wpi/first/networktables/LogMessage.java
index 5d77d8b..fd090af 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/LogMessage.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/LogMessage.java
@@ -5,6 +5,7 @@
package edu.wpi.first.networktables;
/** NetworkTables log message. */
+@SuppressWarnings("MemberName")
public final class LogMessage {
/** Logging levels. */
public static final int kCritical = 50;
@@ -18,49 +19,30 @@
public static final int kDebug3 = 7;
public static final int kDebug4 = 6;
- /** The logger that generated the message. */
- @SuppressWarnings("MemberName")
- public final int logger;
-
/** Log level of the message. */
- @SuppressWarnings("MemberName")
public final int level;
/** The filename of the source file that generated the message. */
- @SuppressWarnings("MemberName")
public final String filename;
/** The line number in the source file that generated the message. */
- @SuppressWarnings("MemberName")
public final int line;
/** The message. */
- @SuppressWarnings("MemberName")
public final String message;
/**
* Constructor. This should generally only be used internally to NetworkTables.
*
- * @param inst Instance
- * @param logger Logger
* @param level Log level
* @param filename Filename
* @param line Line number
* @param message Message
*/
- public LogMessage(
- NetworkTableInstance inst, int logger, int level, String filename, int line, String message) {
- this.m_inst = inst;
- this.logger = logger;
+ public LogMessage(int level, String filename, int line, String message) {
this.level = level;
this.filename = filename;
this.line = line;
this.message = message;
}
-
- private final NetworkTableInstance m_inst;
-
- NetworkTableInstance getInstance() {
- return m_inst;
- }
}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/MultiSubscriber.java b/ntcore/src/main/java/edu/wpi/first/networktables/MultiSubscriber.java
new file mode 100644
index 0000000..8386c1e
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/MultiSubscriber.java
@@ -0,0 +1,61 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.networktables;
+
+/**
+ * Subscribe to multiple topics based on one or more topic name prefixes. Can be used in combination
+ * with ValueListenerPoller to listen for value changes across all matching topics.
+ */
+public final class MultiSubscriber implements AutoCloseable {
+ /**
+ * Create a multiple subscriber.
+ *
+ * @param inst instance
+ * @param prefixes topic name prefixes
+ * @param options subscriber options
+ */
+ public MultiSubscriber(NetworkTableInstance inst, String[] prefixes, PubSubOption... options) {
+ m_inst = inst;
+ m_handle = NetworkTablesJNI.subscribeMultiple(inst.getHandle(), prefixes, options);
+ }
+
+ @Override
+ public synchronized void close() {
+ if (m_handle != 0) {
+ NetworkTablesJNI.unsubscribeMultiple(m_handle);
+ m_handle = 0;
+ }
+ }
+
+ /**
+ * Gets the instance for the subscriber.
+ *
+ * @return Instance
+ */
+ public NetworkTableInstance getInstance() {
+ return m_inst;
+ }
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ public boolean isValid() {
+ return m_handle != 0;
+ }
+
+ /**
+ * Gets the native handle.
+ *
+ * @return Handle
+ */
+ public int getHandle() {
+ return m_handle;
+ }
+
+ private final NetworkTableInstance m_inst;
+ private int m_handle;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NTSendableBuilder.java b/ntcore/src/main/java/edu/wpi/first/networktables/NTSendableBuilder.java
index 65bfbca..4d5e6a5 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NTSendableBuilder.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NTSendableBuilder.java
@@ -5,8 +5,6 @@
package edu.wpi.first.networktables;
import edu.wpi.first.util.sendable.SendableBuilder;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
public interface NTSendableBuilder extends SendableBuilder {
/**
@@ -23,19 +21,9 @@
* function called by setUpdateTable().
*
* @param key property name
- * @return Network table entry
+ * @return Network table topic
*/
- NetworkTableEntry getEntry(String key);
-
- /**
- * Add a NetworkTableValue property.
- *
- * @param key property name
- * @param getter getter function (returns current value)
- * @param setter setter function (sets new value)
- */
- void addValueProperty(
- String key, Supplier<NetworkTableValue> getter, Consumer<NetworkTableValue> setter);
+ Topic getTopic(String key);
/**
* Get the network table.
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
index 7837a21..cc38239 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java
@@ -5,6 +5,7 @@
package edu.wpi.first.networktables;
import java.util.ArrayList;
+import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -129,6 +130,126 @@
return "NetworkTable: " + m_path;
}
+ /**
+ * Get (generic) topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ public Topic getTopic(String name) {
+ return m_inst.getTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get boolean topic.
+ *
+ * @param name topic name
+ * @return BooleanTopic
+ */
+ public BooleanTopic getBooleanTopic(String name) {
+ return m_inst.getBooleanTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get long topic.
+ *
+ * @param name topic name
+ * @return IntegerTopic
+ */
+ public IntegerTopic getIntegerTopic(String name) {
+ return m_inst.getIntegerTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get float topic.
+ *
+ * @param name topic name
+ * @return FloatTopic
+ */
+ public FloatTopic getFloatTopic(String name) {
+ return m_inst.getFloatTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get double topic.
+ *
+ * @param name topic name
+ * @return DoubleTopic
+ */
+ public DoubleTopic getDoubleTopic(String name) {
+ return m_inst.getDoubleTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get String topic.
+ *
+ * @param name topic name
+ * @return StringTopic
+ */
+ public StringTopic getStringTopic(String name) {
+ return m_inst.getStringTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get raw topic.
+ *
+ * @param name topic name
+ * @return RawTopic
+ */
+ public RawTopic getRawTopic(String name) {
+ return m_inst.getRawTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get boolean[] topic.
+ *
+ * @param name topic name
+ * @return BooleanArrayTopic
+ */
+ public BooleanArrayTopic getBooleanArrayTopic(String name) {
+ return m_inst.getBooleanArrayTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get long[] topic.
+ *
+ * @param name topic name
+ * @return IntegerArrayTopic
+ */
+ public IntegerArrayTopic getIntegerArrayTopic(String name) {
+ return m_inst.getIntegerArrayTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get float[] topic.
+ *
+ * @param name topic name
+ * @return FloatArrayTopic
+ */
+ public FloatArrayTopic getFloatArrayTopic(String name) {
+ return m_inst.getFloatArrayTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get double[] topic.
+ *
+ * @param name topic name
+ * @return DoubleArrayTopic
+ */
+ public DoubleArrayTopic getDoubleArrayTopic(String name) {
+ return m_inst.getDoubleArrayTopic(m_pathWithSep + name);
+ }
+
+ /**
+ * Get String[] topic.
+ *
+ * @param name topic name
+ * @return StringArrayTopic
+ */
+ public StringArrayTopic getStringArrayTopic(String name) {
+ return m_inst.getStringArrayTopic(m_pathWithSep + name);
+ }
+
private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>();
/**
@@ -141,106 +262,15 @@
NetworkTableEntry entry = m_entries.get(key);
if (entry == null) {
entry = m_inst.getEntry(m_pathWithSep + key);
- m_entries.putIfAbsent(key, entry);
+ NetworkTableEntry oldEntry = m_entries.putIfAbsent(key, entry);
+ if (oldEntry != null) {
+ entry = oldEntry;
+ }
}
return entry;
}
/**
- * Listen to keys only within this table.
- *
- * @param listener listener to add
- * @param flags {@link EntryListenerFlags} bitmask
- * @return Listener handle
- */
- public int addEntryListener(TableEntryListener listener, int flags) {
- final int prefixLen = m_path.length() + 1;
- return m_inst.addEntryListener(
- m_pathWithSep,
- event -> {
- String relativeKey = event.name.substring(prefixLen);
- if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
- // part of a sub table
- return;
- }
- listener.valueChanged(this, relativeKey, event.getEntry(), event.value, event.flags);
- },
- flags);
- }
-
- /**
- * Listen to a single key.
- *
- * @param key the key name
- * @param listener listener to add
- * @param flags {@link EntryListenerFlags} bitmask
- * @return Listener handle
- */
- public int addEntryListener(String key, TableEntryListener listener, int flags) {
- final NetworkTableEntry entry = getEntry(key);
- return m_inst.addEntryListener(
- entry, event -> listener.valueChanged(this, key, entry, event.value, event.flags), flags);
- }
-
- /**
- * Remove an entry listener.
- *
- * @param listener listener handle
- */
- public void removeEntryListener(int listener) {
- m_inst.removeEntryListener(listener);
- }
-
- /**
- * Listen for sub-table creation. This calls the listener once for each newly created sub-table.
- * It immediately calls the listener for any existing sub-tables.
- *
- * @param listener listener to add
- * @param localNotify notify local changes as well as remote
- * @return Listener handle
- */
- public int addSubTableListener(TableListener listener, boolean localNotify) {
- int flags = EntryListenerFlags.kNew | EntryListenerFlags.kImmediate;
- if (localNotify) {
- flags |= EntryListenerFlags.kLocal;
- }
-
- final int prefixLen = m_path.length() + 1;
- final NetworkTable parent = this;
-
- return m_inst.addEntryListener(
- m_pathWithSep,
- new Consumer<>() {
- final Set<String> m_notifiedTables = new HashSet<>();
-
- @Override
- public void accept(EntryNotification event) {
- String relativeKey = event.name.substring(prefixLen);
- int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
- if (endSubTable == -1) {
- return;
- }
- String subTableKey = relativeKey.substring(0, endSubTable);
- if (m_notifiedTables.contains(subTableKey)) {
- return;
- }
- m_notifiedTables.add(subTableKey);
- listener.tableCreated(parent, subTableKey, parent.getSubTable(subTableKey));
- }
- },
- flags);
- }
-
- /**
- * Remove a sub-table listener.
- *
- * @param listener listener handle
- */
- public void removeTableListener(int listener) {
- m_inst.removeEntryListener(listener);
- }
-
- /**
* Returns the table at the specified key. If there is no table at the specified key, it will
* create a new table
*
@@ -258,7 +288,7 @@
* @return true if the table as a value assigned to the given key
*/
public boolean containsKey(String key) {
- return !("".equals(key)) && getEntry(key).exists();
+ return !("".equals(key)) && getTopic(key).exists();
}
/**
@@ -269,9 +299,64 @@
* its own
*/
public boolean containsSubTable(String key) {
- int[] handles =
- NetworkTablesJNI.getEntries(m_inst.getHandle(), m_pathWithSep + key + PATH_SEPARATOR, 0);
- return handles.length != 0;
+ Topic[] topics = m_inst.getTopics(m_pathWithSep + key + PATH_SEPARATOR, 0);
+ return topics.length != 0;
+ }
+
+ /**
+ * Gets topic information for all keys in the table (not including sub-tables).
+ *
+ * @param types bitmask of types (NetworkTableType values); 0 is treated as a "don't care".
+ * @return topic information for keys currently in the table
+ */
+ public List<TopicInfo> getTopicInfo(int types) {
+ List<TopicInfo> infos = new ArrayList<>();
+ int prefixLen = m_path.length() + 1;
+ for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, types)) {
+ String relativeKey = info.name.substring(prefixLen);
+ if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
+ continue;
+ }
+ infos.add(info);
+ }
+ return infos;
+ }
+
+ /**
+ * Gets topic information for all keys in the table (not including sub-tables).
+ *
+ * @return topic information for keys currently in the table
+ */
+ public List<TopicInfo> getTopicInfo() {
+ return getTopicInfo(0);
+ }
+
+ /**
+ * Gets all topics in the table (not including sub-tables).
+ *
+ * @param types bitmask of types (NetworkTableType values); 0 is treated as a "don't care".
+ * @return topic for keys currently in the table
+ */
+ public List<Topic> getTopics(int types) {
+ List<Topic> topics = new ArrayList<>();
+ int prefixLen = m_path.length() + 1;
+ for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, types)) {
+ String relativeKey = info.name.substring(prefixLen);
+ if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
+ continue;
+ }
+ topics.add(info.getTopic());
+ }
+ return topics;
+ }
+
+ /**
+ * Gets all topics in the table (not including sub-tables).
+ *
+ * @return topic for keys currently in the table
+ */
+ public List<Topic> getTopics() {
+ return getTopics(0);
}
/**
@@ -283,16 +368,12 @@
public Set<String> getKeys(int types) {
Set<String> keys = new HashSet<>();
int prefixLen = m_path.length() + 1;
- for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, types)) {
+ for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, types)) {
String relativeKey = info.name.substring(prefixLen);
if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
continue;
}
keys.add(relativeKey);
- // populate entries as we go
- if (m_entries.get(relativeKey) == null) {
- m_entries.putIfAbsent(relativeKey, new NetworkTableEntry(m_inst, info.entry));
- }
}
return keys;
}
@@ -314,7 +395,7 @@
public Set<String> getSubTables() {
Set<String> keys = new HashSet<>();
int prefixLen = m_path.length() + 1;
- for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, 0)) {
+ for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, 0)) {
String relativeKey = info.name.substring(prefixLen);
int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
if (endSubTable == -1) {
@@ -326,22 +407,13 @@
}
/**
- * Deletes the specified key in this table. The key can not be null.
- *
- * @param key the key name
- */
- public void delete(String key) {
- getEntry(key).delete();
- }
-
- /**
* Put a value in the table.
*
* @param key the key to be assigned to
* @param value the value that will be assigned
* @return False if the table key already exists with a different type
*/
- boolean putValue(String key, NetworkTableValue value) {
+ public boolean putValue(String key, NetworkTableValue value) {
return getEntry(key).setValue(value);
}
@@ -352,7 +424,7 @@
* @param defaultValue the default value to set if key doesn't exist.
* @return False if the table key exists with a different type
*/
- boolean setDefaultValue(String key, NetworkTableValue defaultValue) {
+ public boolean setDefaultValue(String key, NetworkTableValue defaultValue) {
return getEntry(key).setDefaultValue(defaultValue);
}
@@ -362,7 +434,7 @@
* @param key the key of the value to look up
* @return the value associated with the given key, or nullptr if the key does not exist
*/
- NetworkTableValue getValue(String key) {
+ public NetworkTableValue getValue(String key) {
return getEntry(key).getValue();
}
@@ -375,26 +447,121 @@
return m_path;
}
- /**
- * Save table values to a file. The file format used is identical to that used for SavePersistent.
- *
- * @param filename filename
- * @throws PersistentException if error saving file
- */
- public void saveEntries(String filename) throws PersistentException {
- m_inst.saveEntries(filename, m_pathWithSep);
+ /** A listener that listens to events on topics in a {@link NetworkTable}. */
+ @FunctionalInterface
+ public interface TableEventListener {
+ /**
+ * Called when an event occurs on a topic in a {@link NetworkTable}.
+ *
+ * @param table the table the topic exists in
+ * @param key the key associated with the topic that changed
+ * @param event the event
+ */
+ void accept(NetworkTable table, String key, NetworkTableEvent event);
}
/**
- * Load table values from a file. The file format used is identical to that used for
- * SavePersistent / LoadPersistent.
+ * Listen to topics only within this table.
*
- * @param filename filename
- * @return List of warnings (errors result in an exception instead)
- * @throws PersistentException if error saving file
+ * @param eventKinds set of event kinds to listen to
+ * @param listener listener to add
+ * @return Listener handle
*/
- public String[] loadEntries(String filename) throws PersistentException {
- return m_inst.loadEntries(filename, m_pathWithSep);
+ public int addListener(EnumSet<NetworkTableEvent.Kind> eventKinds, TableEventListener listener) {
+ final int prefixLen = m_path.length() + 1;
+ return m_inst.addListener(
+ new String[] {m_pathWithSep},
+ eventKinds,
+ event -> {
+ String topicName = null;
+ if (event.topicInfo != null) {
+ topicName = event.topicInfo.name;
+ } else if (event.valueData != null) {
+ topicName = event.valueData.getTopic().getName();
+ }
+ if (topicName == null) {
+ return;
+ }
+ String relativeKey = topicName.substring(prefixLen);
+ if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
+ // part of a sub table
+ return;
+ }
+ listener.accept(this, relativeKey, event);
+ });
+ }
+
+ /**
+ * Listen to a single key.
+ *
+ * @param key the key name
+ * @param eventKinds set of event kinds to listen to
+ * @param listener listener to add
+ * @return Listener handle
+ */
+ public int addListener(
+ String key, EnumSet<NetworkTableEvent.Kind> eventKinds, TableEventListener listener) {
+ NetworkTableEntry entry = getEntry(key);
+ return m_inst.addListener(entry, eventKinds, event -> listener.accept(this, key, event));
+ }
+
+ /** A listener that listens to new tables in a {@link NetworkTable}. */
+ @FunctionalInterface
+ public interface SubTableListener {
+ /**
+ * Called when a new table is created within a {@link NetworkTable}.
+ *
+ * @param parent the parent of the table
+ * @param name the name of the new table
+ * @param table the new table
+ */
+ void tableCreated(NetworkTable parent, String name, NetworkTable table);
+ }
+
+ /**
+ * Listen for sub-table creation. This calls the listener once for each newly created sub-table.
+ * It immediately calls the listener for any existing sub-tables.
+ *
+ * @param listener listener to add
+ * @return Listener handle
+ */
+ public int addSubTableListener(SubTableListener listener) {
+ final int prefixLen = m_path.length() + 1;
+ final NetworkTable parent = this;
+
+ return m_inst.addListener(
+ new String[] {m_pathWithSep},
+ EnumSet.of(NetworkTableEvent.Kind.kPublish, NetworkTableEvent.Kind.kImmediate),
+ new Consumer<NetworkTableEvent>() {
+ final Set<String> m_notifiedTables = new HashSet<>();
+
+ @Override
+ public void accept(NetworkTableEvent event) {
+ if (event.topicInfo == null) {
+ return; // should not happen
+ }
+ String relativeKey = event.topicInfo.name.substring(prefixLen);
+ int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
+ if (endSubTable == -1) {
+ return;
+ }
+ String subTableKey = relativeKey.substring(0, endSubTable);
+ if (m_notifiedTables.contains(subTableKey)) {
+ return;
+ }
+ m_notifiedTables.add(subTableKey);
+ listener.tableCreated(parent, subTableKey, parent.getSubTable(subTableKey));
+ }
+ });
+ }
+
+ /**
+ * Remove a listener.
+ *
+ * @param listener listener handle
+ */
+ public void removeListener(int listener) {
+ m_inst.removeListener(listener);
}
@Override
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java
deleted file mode 100644
index 872bc89..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java
+++ /dev/null
@@ -1,858 +0,0 @@
-// 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.nio.ByteBuffer;
-import java.util.function.Consumer;
-
-/** NetworkTables Entry. */
-public final class NetworkTableEntry {
- /** Flag values (as returned by {@link #getFlags()}). */
- public static final int kPersistent = 0x01;
-
- /**
- * Construct from native handle.
- *
- * @param inst Instance
- * @param handle Native handle
- */
- public NetworkTableEntry(NetworkTableInstance inst, int handle) {
- m_inst = inst;
- m_handle = handle;
- }
-
- /**
- * Determines if the native handle is valid.
- *
- * @return True if the native handle is valid, false otherwise.
- */
- public boolean isValid() {
- return m_handle != 0;
- }
-
- /**
- * Gets the native handle for the entry.
- *
- * @return Native handle
- */
- public int getHandle() {
- return m_handle;
- }
-
- /**
- * Gets the instance for the entry.
- *
- * @return Instance
- */
- public NetworkTableInstance getInstance() {
- return m_inst;
- }
-
- /**
- * Determines if the entry currently exists.
- *
- * @return True if the entry exists, false otherwise.
- */
- public boolean exists() {
- return NetworkTablesJNI.getType(m_handle) != 0;
- }
-
- /**
- * Gets the name of the entry (the key).
- *
- * @return the entry's name
- */
- public String getName() {
- return NetworkTablesJNI.getEntryName(m_handle);
- }
-
- /**
- * Gets the type of the entry.
- *
- * @return the entry's type
- */
- public NetworkTableType getType() {
- return NetworkTableType.getFromInt(NetworkTablesJNI.getType(m_handle));
- }
-
- /**
- * Returns the flags.
- *
- * @return the flags (bitmask)
- */
- public int getFlags() {
- return NetworkTablesJNI.getEntryFlags(m_handle);
- }
-
- /**
- * Gets the last time the entry's value was changed.
- *
- * @return Entry last change time
- */
- public long getLastChange() {
- return NetworkTablesJNI.getEntryLastChange(m_handle);
- }
-
- /**
- * Gets combined information about the entry.
- *
- * @return Entry information
- */
- public EntryInfo getInfo() {
- return NetworkTablesJNI.getEntryInfoHandle(m_inst, m_handle);
- }
-
- /**
- * Gets the entry's value. Returns a value with type NetworkTableType.kUnassigned if the value
- * does not exist.
- *
- * @return the entry's value
- */
- public NetworkTableValue getValue() {
- return NetworkTablesJNI.getValue(m_handle);
- }
-
- /**
- * Gets the entry's value as a boolean. If the entry does not exist or is of different type, it
- * will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public boolean getBoolean(boolean defaultValue) {
- return NetworkTablesJNI.getBoolean(m_handle, defaultValue);
- }
-
- /**
- * Gets the entry's value as a double. If the entry does not exist or is of different type, it
- * will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public double getDouble(double defaultValue) {
- return NetworkTablesJNI.getDouble(m_handle, defaultValue);
- }
-
- /**
- * Gets the entry's value as a double. If the entry does not exist or is of different type, it
- * will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public Number getNumber(Number defaultValue) {
- return NetworkTablesJNI.getDouble(m_handle, defaultValue.doubleValue());
- }
-
- /**
- * Gets the entry's value as a string. If the entry does not exist or is of different type, it
- * will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public String getString(String defaultValue) {
- return NetworkTablesJNI.getString(m_handle, defaultValue);
- }
-
- /**
- * Gets the entry's value as a raw value (byte array). If the entry does not exist or is of
- * different type, it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public byte[] getRaw(byte[] defaultValue) {
- return NetworkTablesJNI.getRaw(m_handle, defaultValue);
- }
-
- /**
- * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
- * it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public boolean[] getBooleanArray(boolean[] defaultValue) {
- return NetworkTablesJNI.getBooleanArray(m_handle, defaultValue);
- }
-
- /**
- * Gets the entry's value as a boolean array. If the entry does not exist or is of different type,
- * it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public Boolean[] getBooleanArray(Boolean[] defaultValue) {
- return NetworkTableValue.fromNative(
- NetworkTablesJNI.getBooleanArray(m_handle, NetworkTableValue.toNative(defaultValue)));
- }
-
- /**
- * Gets the entry's value as a double array. If the entry does not exist or is of different type,
- * it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public double[] getDoubleArray(double[] defaultValue) {
- return NetworkTablesJNI.getDoubleArray(m_handle, defaultValue);
- }
-
- /**
- * Gets the entry's value as a double array. If the entry does not exist or is of different type,
- * it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public Double[] getDoubleArray(Double[] defaultValue) {
- return NetworkTableValue.fromNative(
- NetworkTablesJNI.getDoubleArray(m_handle, NetworkTableValue.toNative(defaultValue)));
- }
-
- /**
- * Gets the entry's value as a double array. If the entry does not exist or is of different type,
- * it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public Number[] getNumberArray(Number[] defaultValue) {
- return NetworkTableValue.fromNative(
- NetworkTablesJNI.getDoubleArray(m_handle, NetworkTableValue.toNative(defaultValue)));
- }
-
- /**
- * Gets the entry's value as a string array. If the entry does not exist or is of different type,
- * it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- */
- public String[] getStringArray(String[] defaultValue) {
- return NetworkTablesJNI.getStringArray(m_handle, defaultValue);
- }
-
- /**
- * Checks if a data value is of a type that can be placed in a NetworkTable entry.
- *
- * @param data the data to check
- * @return true if the data can be placed in an entry, false if it cannot
- */
- public static boolean isValidDataType(Object data) {
- return data instanceof Number
- || data instanceof Boolean
- || data instanceof String
- || data instanceof double[]
- || data instanceof Double[]
- || data instanceof Number[]
- || data instanceof boolean[]
- || data instanceof Boolean[]
- || data instanceof String[]
- || data instanceof byte[]
- || data instanceof Byte[];
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- * @throws IllegalArgumentException if the value is not a known type
- */
- public boolean setDefaultValue(Object defaultValue) {
- if (defaultValue instanceof NetworkTableValue) {
- long time = ((NetworkTableValue) defaultValue).getTime();
- Object otherValue = ((NetworkTableValue) defaultValue).getValue();
- switch (((NetworkTableValue) defaultValue).getType()) {
- case kBoolean:
- return NetworkTablesJNI.setDefaultBoolean(m_handle, time, (Boolean) otherValue);
- case kDouble:
- return NetworkTablesJNI.setDefaultDouble(
- m_handle, time, ((Number) otherValue).doubleValue());
- case kString:
- return NetworkTablesJNI.setDefaultString(m_handle, time, (String) otherValue);
- case kRaw:
- return NetworkTablesJNI.setDefaultRaw(m_handle, time, (byte[]) otherValue);
- case kBooleanArray:
- return NetworkTablesJNI.setDefaultBooleanArray(m_handle, time, (boolean[]) otherValue);
- case kDoubleArray:
- return NetworkTablesJNI.setDefaultDoubleArray(m_handle, time, (double[]) otherValue);
- case kStringArray:
- return NetworkTablesJNI.setDefaultStringArray(m_handle, time, (String[]) otherValue);
- case kRpc:
- // TODO
- default:
- return true;
- }
- } else if (defaultValue instanceof Boolean) {
- return setDefaultBoolean((Boolean) defaultValue);
- } else if (defaultValue instanceof Number) {
- return setDefaultNumber((Number) defaultValue);
- } else if (defaultValue instanceof String) {
- return setDefaultString((String) defaultValue);
- } else if (defaultValue instanceof byte[]) {
- return setDefaultRaw((byte[]) defaultValue);
- } else if (defaultValue instanceof boolean[]) {
- return setDefaultBooleanArray((boolean[]) defaultValue);
- } else if (defaultValue instanceof double[]) {
- return setDefaultDoubleArray((double[]) defaultValue);
- } else if (defaultValue instanceof Boolean[]) {
- return setDefaultBooleanArray((Boolean[]) defaultValue);
- } else if (defaultValue instanceof Number[]) {
- return setDefaultNumberArray((Number[]) defaultValue);
- } else if (defaultValue instanceof String[]) {
- return setDefaultStringArray((String[]) defaultValue);
- } else {
- throw new IllegalArgumentException(
- "Value of type " + defaultValue.getClass().getName() + " cannot be put into a table");
- }
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultBoolean(boolean defaultValue) {
- return NetworkTablesJNI.setDefaultBoolean(m_handle, 0, defaultValue);
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultDouble(double defaultValue) {
- return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue);
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultNumber(Number defaultValue) {
- return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue.doubleValue());
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultString(String defaultValue) {
- return NetworkTablesJNI.setDefaultString(m_handle, 0, defaultValue);
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultRaw(byte[] defaultValue) {
- return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue);
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultBooleanArray(boolean[] defaultValue) {
- return NetworkTablesJNI.setDefaultBooleanArray(m_handle, 0, defaultValue);
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultBooleanArray(Boolean[] defaultValue) {
- return NetworkTablesJNI.setDefaultBooleanArray(
- m_handle, 0, NetworkTableValue.toNative(defaultValue));
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultDoubleArray(double[] defaultValue) {
- return NetworkTablesJNI.setDefaultDoubleArray(m_handle, 0, defaultValue);
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultNumberArray(Number[] defaultValue) {
- return NetworkTablesJNI.setDefaultDoubleArray(
- m_handle, 0, NetworkTableValue.toNative(defaultValue));
- }
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDefaultStringArray(String[] defaultValue) {
- return NetworkTablesJNI.setDefaultStringArray(m_handle, 0, defaultValue);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value that will be assigned
- * @return False if the table key already exists with a different type
- * @throws IllegalArgumentException if the value is not a known type
- */
- public boolean setValue(Object value) {
- if (value instanceof NetworkTableValue) {
- long time = ((NetworkTableValue) value).getTime();
- Object otherValue = ((NetworkTableValue) value).getValue();
- switch (((NetworkTableValue) value).getType()) {
- case kBoolean:
- return NetworkTablesJNI.setBoolean(m_handle, time, (Boolean) otherValue, false);
- case kDouble:
- return NetworkTablesJNI.setDouble(
- m_handle, time, ((Number) otherValue).doubleValue(), false);
- case kString:
- return NetworkTablesJNI.setString(m_handle, time, (String) otherValue, false);
- case kRaw:
- return NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue, false);
- case kBooleanArray:
- return NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue, false);
- case kDoubleArray:
- return NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue, false);
- case kStringArray:
- return NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue, false);
- case kRpc:
- // TODO
- default:
- return true;
- }
- } else if (value instanceof Boolean) {
- return setBoolean((Boolean) value);
- } else if (value instanceof Number) {
- return setNumber((Number) value);
- } else if (value instanceof String) {
- return setString((String) value);
- } else if (value instanceof byte[]) {
- return setRaw((byte[]) value);
- } else if (value instanceof boolean[]) {
- return setBooleanArray((boolean[]) value);
- } else if (value instanceof double[]) {
- return setDoubleArray((double[]) value);
- } else if (value instanceof Boolean[]) {
- return setBooleanArray((Boolean[]) value);
- } else if (value instanceof Number[]) {
- return setNumberArray((Number[]) value);
- } else if (value instanceof String[]) {
- return setStringArray((String[]) value);
- } else {
- throw new IllegalArgumentException(
- "Value of type " + value.getClass().getName() + " cannot be put into a table");
- }
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setBoolean(boolean value) {
- return NetworkTablesJNI.setBoolean(m_handle, 0, value, false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDouble(double value) {
- return NetworkTablesJNI.setDouble(m_handle, 0, value, false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setNumber(Number value) {
- return NetworkTablesJNI.setDouble(m_handle, 0, value.doubleValue(), false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setString(String value) {
- return NetworkTablesJNI.setString(m_handle, 0, value, false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setRaw(byte[] value) {
- return NetworkTablesJNI.setRaw(m_handle, 0, value, false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @param len the length of the value
- * @return False if the entry exists with a different type
- */
- public boolean setRaw(ByteBuffer value, int len) {
- if (!value.isDirect()) {
- throw new IllegalArgumentException("must be a direct buffer");
- }
- if (value.capacity() < len) {
- throw new IllegalArgumentException("buffer is too small, must be at least " + len);
- }
- return NetworkTablesJNI.setRaw(m_handle, 0, value, len, false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setBooleanArray(boolean[] value) {
- return NetworkTablesJNI.setBooleanArray(m_handle, 0, value, false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setBooleanArray(Boolean[] value) {
- return NetworkTablesJNI.setBooleanArray(m_handle, 0, NetworkTableValue.toNative(value), false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setDoubleArray(double[] value) {
- return NetworkTablesJNI.setDoubleArray(m_handle, 0, value, false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setNumberArray(Number[] value) {
- return NetworkTablesJNI.setDoubleArray(m_handle, 0, NetworkTableValue.toNative(value), false);
- }
-
- /**
- * Sets the entry's value.
- *
- * @param value the value to set
- * @return False if the entry exists with a different type
- */
- public boolean setStringArray(String[] value) {
- return NetworkTablesJNI.setStringArray(m_handle, 0, value, false);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- * @throws IllegalArgumentException if the value is not a known type
- */
- public void forceSetValue(Object value) {
- if (value instanceof NetworkTableValue) {
- long time = ((NetworkTableValue) value).getTime();
- Object otherValue = ((NetworkTableValue) value).getValue();
- switch (((NetworkTableValue) value).getType()) {
- case kBoolean:
- NetworkTablesJNI.setBoolean(m_handle, time, (Boolean) otherValue, true);
- return;
- case kDouble:
- NetworkTablesJNI.setDouble(m_handle, time, ((Number) otherValue).doubleValue(), true);
- return;
- case kString:
- NetworkTablesJNI.setString(m_handle, time, (String) otherValue, true);
- return;
- case kRaw:
- NetworkTablesJNI.setRaw(m_handle, time, (byte[]) otherValue, true);
- return;
- case kBooleanArray:
- NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[]) otherValue, true);
- return;
- case kDoubleArray:
- NetworkTablesJNI.setDoubleArray(m_handle, time, (double[]) otherValue, true);
- return;
- case kStringArray:
- NetworkTablesJNI.setStringArray(m_handle, time, (String[]) otherValue, true);
- return;
- case kRpc:
- // TODO
- default:
- return;
- }
- } else if (value instanceof Boolean) {
- forceSetBoolean((Boolean) value);
- } else if (value instanceof Number) {
- forceSetNumber((Number) value);
- } else if (value instanceof String) {
- forceSetString((String) value);
- } else if (value instanceof byte[]) {
- forceSetRaw((byte[]) value);
- } else if (value instanceof boolean[]) {
- forceSetBooleanArray((boolean[]) value);
- } else if (value instanceof double[]) {
- forceSetDoubleArray((double[]) value);
- } else if (value instanceof Boolean[]) {
- forceSetBooleanArray((Boolean[]) value);
- } else if (value instanceof Number[]) {
- forceSetNumberArray((Number[]) value);
- } else if (value instanceof String[]) {
- forceSetStringArray((String[]) value);
- } else {
- throw new IllegalArgumentException(
- "Value of type " + value.getClass().getName() + " cannot be put into a table");
- }
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetBoolean(boolean value) {
- NetworkTablesJNI.setBoolean(m_handle, 0, value, true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetDouble(double value) {
- NetworkTablesJNI.setDouble(m_handle, 0, value, true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetNumber(Number value) {
- NetworkTablesJNI.setDouble(m_handle, 0, value.doubleValue(), true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetString(String value) {
- NetworkTablesJNI.setString(m_handle, 0, value, true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetRaw(byte[] value) {
- NetworkTablesJNI.setRaw(m_handle, 0, value, true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetBooleanArray(boolean[] value) {
- NetworkTablesJNI.setBooleanArray(m_handle, 0, value, true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetBooleanArray(Boolean[] value) {
- NetworkTablesJNI.setBooleanArray(m_handle, 0, NetworkTableValue.toNative(value), true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetDoubleArray(double[] value) {
- NetworkTablesJNI.setDoubleArray(m_handle, 0, value, true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetNumberArray(Number[] value) {
- NetworkTablesJNI.setDoubleArray(m_handle, 0, NetworkTableValue.toNative(value), true);
- }
-
- /**
- * Sets the entry's value. If the value is of different type, the type is changed to match the new
- * value.
- *
- * @param value the value to set
- */
- public void forceSetStringArray(String[] value) {
- NetworkTablesJNI.setStringArray(m_handle, 0, value, true);
- }
-
- /**
- * Sets flags.
- *
- * @param flags the flags to set (bitmask)
- */
- public void setFlags(int flags) {
- NetworkTablesJNI.setEntryFlags(m_handle, getFlags() | flags);
- }
-
- /**
- * Clears flags.
- *
- * @param flags the flags to clear (bitmask)
- */
- public void clearFlags(int flags) {
- NetworkTablesJNI.setEntryFlags(m_handle, getFlags() & ~flags);
- }
-
- /** Make value persistent through program restarts. */
- public void setPersistent() {
- setFlags(kPersistent);
- }
-
- /** Stop making value persistent through program restarts. */
- public void clearPersistent() {
- clearFlags(kPersistent);
- }
-
- /**
- * Returns whether the value is persistent through program restarts.
- *
- * @return True if the value is persistent.
- */
- public boolean isPersistent() {
- return (getFlags() & kPersistent) != 0;
- }
-
- /** Deletes the entry. */
- public void delete() {
- NetworkTablesJNI.deleteEntry(m_handle);
- }
-
- /**
- * Create a callback-based RPC entry point. Only valid to use on the server. The callback function
- * will be called when the RPC is called. This function creates RPC version 0 definitions (raw
- * data in and out).
- *
- * @param callback callback function
- */
- public void createRpc(Consumer<RpcAnswer> callback) {
- m_inst.createRpc(this, callback);
- }
-
- /**
- * Call a RPC function. May be used on either the client or server. This function is non-blocking.
- * Either {@link RpcCall#getResult()} or {@link RpcCall#cancelResult()} must be called on the
- * return value to either get or ignore the result of the call.
- *
- * @param params parameter
- * @return RPC call object.
- */
- public RpcCall callRpc(byte[] params) {
- return new RpcCall(this, NetworkTablesJNI.callRpc(m_handle, params));
- }
-
- /**
- * Add a listener for changes to the entry.
- *
- * @param listener the listener to add
- * @param flags bitmask specifying desired notifications
- * @return listener handle
- */
- public int addListener(Consumer<EntryNotification> listener, int flags) {
- return m_inst.addEntryListener(this, listener, flags);
- }
-
- /**
- * Remove a listener from receiving entry events.
- *
- * @param listener the listener to be removed
- */
- public void removeListener(int listener) {
- m_inst.removeEntryListener(listener);
- }
-
- @Override
- public boolean equals(Object other) {
- if (other == this) {
- return true;
- }
- if (!(other instanceof NetworkTableEntry)) {
- return false;
- }
-
- return m_handle == ((NetworkTableEntry) other).m_handle;
- }
-
- @Override
- public int hashCode() {
- return m_handle;
- }
-
- private final NetworkTableInstance m_inst;
- private final int m_handle;
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEvent.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEvent.java
new file mode 100644
index 0000000..97883c8
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEvent.java
@@ -0,0 +1,152 @@
+// 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 event.
+ *
+ * <p>There are different kinds of events. When creating a listener, a combination of event kinds
+ * can be listened to by building an EnumSet of NetworkTableEvent.Kind.
+ */
+@SuppressWarnings("MemberName")
+public final class NetworkTableEvent {
+ public enum Kind {
+ /**
+ * Initial listener addition. Set this to receive immediate notification of matches to other
+ * criteria.
+ */
+ kImmediate(0x0001),
+
+ /** Client connected (on server, any client connected). */
+ kConnected(0x0002),
+
+ /** Client disconnected (on server, any client disconnected). */
+ kDisconnected(0x0004),
+
+ /** Any connection event (connect or disconnect). */
+ kConnection(0x0004 | 0x0002),
+
+ /** New topic published. */
+ kPublish(0x0008),
+
+ /** Topic unpublished. */
+ kUnpublish(0x0010),
+
+ /** Topic properties changed. */
+ kProperties(0x0020),
+
+ /** Any topic event (publish, unpublish, or properties changed). */
+ kTopic(0x0020 | 0x0010 | 0x0008),
+
+ /** Topic value updated (via network). */
+ kValueRemote(0x0040),
+
+ /** Topic value updated (local). */
+ kValueLocal(0x0080),
+
+ /** Topic value updated (network or local). */
+ kValueAll(0x0080 | 0x0040),
+
+ /** Log message. */
+ kLogMessage(0x0100),
+
+ /** Time synchronized with server. */
+ kTimeSync(0x0200);
+
+ private final int value;
+
+ Kind(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ /**
+ * Handle of listener that was triggered. The value returned when adding the listener can be used
+ * to map this to a specific added listener.
+ */
+ public final int listener;
+
+ /**
+ * Determine if event is of a particular kind. For example, kPublish if the topic was not
+ * previously published. Also indicates the data included with the event:
+ *
+ * <ul>
+ * <li>kConnected or kDisconnected: connInfo
+ * <li>kPublish, kUnpublish, or kProperties: topicInfo
+ * <li>kValueRemote, kValueLocal: valueData
+ * <li>kLogMessage: logMessage
+ * </ul>
+ *
+ * @param kind Kind
+ * @return True if event matches kind
+ */
+ public boolean is(Kind kind) {
+ return (m_flags & kind.getValue()) != 0;
+ }
+
+ private final int m_flags;
+
+ /** Connection information (for connection events). */
+ public final ConnectionInfo connInfo;
+
+ /** Topic information (for topic events). */
+ public final TopicInfo topicInfo;
+
+ /** Value data (for value events). */
+ public final ValueEventData valueData;
+
+ /** Log message (for log message events). */
+ public final LogMessage logMessage;
+
+ /** Log message (for log message events). */
+ public final TimeSyncEventData timeSyncData;
+
+ /**
+ * Constructor. This should generally only be used internally to NetworkTables.
+ *
+ * @param inst Instance
+ * @param listener Listener that was triggered
+ * @param flags Event flags
+ * @param connInfo Connection information
+ * @param topicInfo Topic information
+ * @param valueData Value data
+ * @param logMessage Log message
+ * @param timeSyncData Time sync data
+ */
+ public NetworkTableEvent(
+ NetworkTableInstance inst,
+ int listener,
+ int flags,
+ ConnectionInfo connInfo,
+ TopicInfo topicInfo,
+ ValueEventData valueData,
+ LogMessage logMessage,
+ TimeSyncEventData timeSyncData) {
+ this.m_inst = inst;
+ this.listener = listener;
+ this.m_flags = flags;
+ this.connInfo = connInfo;
+ this.topicInfo = topicInfo;
+ this.valueData = valueData;
+ this.logMessage = logMessage;
+ this.timeSyncData = timeSyncData;
+ }
+
+ /* Network table instance. */
+ private final NetworkTableInstance m_inst;
+
+ /**
+ * Gets the instance associated with this event.
+ *
+ * @return Instance
+ */
+ public NetworkTableInstance getInstance() {
+ return m_inst;
+ }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java
deleted file mode 100644
index 162327e..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java
+++ /dev/null
@@ -1,1153 +0,0 @@
-// 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.HashMap;
-import java.util.Map;
-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;
-
-/**
- * NetworkTables Instance.
- *
- * <p>Instances are completely independent from each other. Table operations on one instance will
- * not be visible to other instances unless the instances are connected via the network. The main
- * limitation on instances is that you cannot have two servers on the same network port. The main
- * utility of instances is for unit testing, but they can also enable one program to connect to two
- * different NetworkTables networks.
- *
- * <p>The global "default" instance (as returned by {@link #getDefault()}) is always available, and
- * is intended for the common case when there is only a single NetworkTables instance being used in
- * the program.
- *
- * <p>Additional instances can be created with the {@link #create()} function. A reference must be
- * kept to the NetworkTableInstance returned by this function to keep it from being garbage
- * collected.
- */
-public final class NetworkTableInstance implements AutoCloseable {
- /**
- * Client/server mode flag values (as returned by {@link #getNetworkMode()}). This is a bitmask.
- */
- public static final int kNetModeNone = 0x00;
-
- public static final int kNetModeServer = 0x01;
- public static final int kNetModeClient = 0x02;
- public static final int kNetModeStarting = 0x04;
- public static final int kNetModeFailure = 0x08;
- public static final int kNetModeLocal = 0x10;
-
- /** The default port that network tables operates on. */
- public static final int kDefaultPort = 1735;
-
- /**
- * Construct from native handle.
- *
- * @param handle Native handle
- */
- private NetworkTableInstance(int handle) {
- m_owned = false;
- m_handle = handle;
- }
-
- /** Destroys the instance (if created by {@link #create()}). */
- @Override
- public synchronized void close() {
- if (m_owned && m_handle != 0) {
- NetworkTablesJNI.destroyInstance(m_handle);
- }
- }
-
- /**
- * Determines if the native handle is valid.
- *
- * @return True if the native handle is valid, false otherwise.
- */
- public boolean isValid() {
- return m_handle != 0;
- }
-
- /* The default instance. */
- private static NetworkTableInstance s_defaultInstance;
-
- /**
- * Get global default instance.
- *
- * @return Global default instance
- */
- public static synchronized NetworkTableInstance getDefault() {
- if (s_defaultInstance == null) {
- s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance());
- }
- return s_defaultInstance;
- }
-
- /**
- * Create an instance. Note: A reference to the returned instance must be retained to ensure the
- * instance is not garbage collected.
- *
- * @return Newly created instance
- */
- public static NetworkTableInstance create() {
- NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance());
- inst.m_owned = true;
- return inst;
- }
-
- /**
- * Gets the native handle for the entry.
- *
- * @return Native handle
- */
- public int getHandle() {
- return m_handle;
- }
-
- /**
- * Gets the entry for a key.
- *
- * @param name Key
- * @return Network table entry.
- */
- public NetworkTableEntry getEntry(String name) {
- return new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name));
- }
-
- /**
- * Get entries starting with the given prefix. The results are optionally filtered by string
- * prefix and entry type to only return a subset of all entries.
- *
- * @param prefix entry name required prefix; only entries whose name starts with this string are
- * returned
- * @param types bitmask of types; 0 is treated as a "don't care"
- * @return Array of entries.
- */
- public NetworkTableEntry[] getEntries(String prefix, int types) {
- int[] handles = NetworkTablesJNI.getEntries(m_handle, prefix, types);
- NetworkTableEntry[] entries = new NetworkTableEntry[handles.length];
- for (int i = 0; i < handles.length; i++) {
- entries[i] = new NetworkTableEntry(this, handles[i]);
- }
- return entries;
- }
-
- /**
- * Get information about entries starting with the given prefix. The results are optionally
- * filtered by string prefix and entry type to only return a subset of all entries.
- *
- * @param prefix entry name required prefix; only entries whose name starts with this string are
- * returned
- * @param types bitmask of types; 0 is treated as a "don't care"
- * @return Array of entry information.
- */
- public EntryInfo[] getEntryInfo(String prefix, int types) {
- return NetworkTablesJNI.getEntryInfo(this, m_handle, prefix, types);
- }
-
- /* Cache of created tables. */
- private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>();
-
- /**
- * Gets the table with the specified key.
- *
- * @param key the key name
- * @return The network table
- */
- public NetworkTable getTable(String key) {
- // prepend leading / if not present
- String theKey;
- if (key.isEmpty() || "/".equals(key)) {
- theKey = "";
- } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
- theKey = key;
- } else {
- theKey = NetworkTable.PATH_SEPARATOR + key;
- }
-
- // cache created tables
- NetworkTable table = m_tables.get(theKey);
- if (table == null) {
- table = new NetworkTable(this, theKey);
- NetworkTable oldTable = m_tables.putIfAbsent(theKey, table);
- if (oldTable != null) {
- table = oldTable;
- }
- }
- return table;
- }
-
- /** Deletes ALL keys in ALL subtables (except persistent values). Use with caution! */
- public void deleteAllEntries() {
- NetworkTablesJNI.deleteAllEntries(m_handle);
- }
-
- /*
- * Callback Creation Functions
- */
-
- private static class EntryConsumer<T> {
- final NetworkTableEntry m_entry;
- final Consumer<T> m_consumer;
-
- EntryConsumer(NetworkTableEntry entry, Consumer<T> consumer) {
- m_entry = entry;
- m_consumer = consumer;
- }
- }
-
- private final ReentrantLock m_entryListenerLock = new ReentrantLock();
- private final Map<Integer, EntryConsumer<EntryNotification>> m_entryListeners = new HashMap<>();
- private int m_entryListenerPoller;
- private boolean m_entryListenerWaitQueue;
- private final Condition m_entryListenerWaitQueueCond = m_entryListenerLock.newCondition();
-
- @SuppressWarnings("PMD.AvoidCatchingThrowable")
- private void startEntryListenerThread() {
- var entryListenerThread =
- new Thread(
- () -> {
- boolean wasInterrupted = false;
- while (!Thread.interrupted()) {
- EntryNotification[] events;
- try {
- events = NetworkTablesJNI.pollEntryListener(this, m_entryListenerPoller);
- } catch (InterruptedException ex) {
- m_entryListenerLock.lock();
- try {
- if (m_entryListenerWaitQueue) {
- m_entryListenerWaitQueue = false;
- m_entryListenerWaitQueueCond.signalAll();
- continue;
- }
- } finally {
- m_entryListenerLock.unlock();
- }
- Thread.currentThread().interrupt();
- // don't try to destroy poller, as its handle is likely no longer valid
- wasInterrupted = true;
- break;
- }
- for (EntryNotification event : events) {
- EntryConsumer<EntryNotification> listener;
- m_entryListenerLock.lock();
- try {
- listener = m_entryListeners.get(event.listener);
- } finally {
- m_entryListenerLock.unlock();
- }
- if (listener != null) {
- event.m_entryObject = listener.m_entry;
- try {
- listener.m_consumer.accept(event);
- } catch (Throwable throwable) {
- System.err.println(
- "Unhandled exception during entry listener callback: "
- + throwable.toString());
- throwable.printStackTrace();
- }
- }
- }
- }
- m_entryListenerLock.lock();
- try {
- if (!wasInterrupted) {
- NetworkTablesJNI.destroyEntryListenerPoller(m_entryListenerPoller);
- }
- m_entryListenerPoller = 0;
- } finally {
- m_entryListenerLock.unlock();
- }
- },
- "NTEntryListener");
- entryListenerThread.setDaemon(true);
- entryListenerThread.start();
- }
-
- /**
- * Add a listener for all entries starting with a certain prefix.
- *
- * @param prefix UTF-8 string prefix
- * @param listener listener to add
- * @param flags {@link EntryListenerFlags} bitmask
- * @return Listener handle
- */
- public int addEntryListener(String prefix, Consumer<EntryNotification> listener, int flags) {
- m_entryListenerLock.lock();
- try {
- if (m_entryListenerPoller == 0) {
- m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
- startEntryListenerThread();
- }
- int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, prefix, flags);
- m_entryListeners.put(handle, new EntryConsumer<>(null, listener));
- return handle;
- } finally {
- m_entryListenerLock.unlock();
- }
- }
-
- /**
- * Add a listener for a particular entry.
- *
- * @param entry the entry
- * @param listener listener to add
- * @param flags {@link EntryListenerFlags} bitmask
- * @return Listener handle
- */
- public int addEntryListener(
- NetworkTableEntry entry, Consumer<EntryNotification> listener, int flags) {
- if (!equals(entry.getInstance())) {
- throw new IllegalArgumentException("entry does not belong to this instance");
- }
- m_entryListenerLock.lock();
- try {
- if (m_entryListenerPoller == 0) {
- m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
- startEntryListenerThread();
- }
- int handle =
- NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, entry.getHandle(), flags);
- m_entryListeners.put(handle, new EntryConsumer<>(entry, listener));
- return handle;
- } finally {
- m_entryListenerLock.unlock();
- }
- }
-
- /**
- * Remove an entry listener.
- *
- * @param listener Listener handle to remove
- */
- public void removeEntryListener(int listener) {
- NetworkTablesJNI.removeEntryListener(listener);
- }
-
- /**
- * Wait for the entry listener queue to be empty. This is primarily useful for deterministic
- * testing. This blocks until either the entry listener queue is empty (e.g. there are no more
- * events that need to be passed along to callbacks or poll queues) or the timeout expires.
- *
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
- * block indefinitely
- * @return False if timed out, otherwise true.
- */
- public boolean waitForEntryListenerQueue(double timeout) {
- if (!NetworkTablesJNI.waitForEntryListenerQueue(m_handle, timeout)) {
- return false;
- }
- m_entryListenerLock.lock();
- try {
- if (m_entryListenerPoller != 0) {
- m_entryListenerWaitQueue = true;
- NetworkTablesJNI.cancelPollEntryListener(m_entryListenerPoller);
- while (m_entryListenerWaitQueue) {
- try {
- if (timeout < 0) {
- m_entryListenerWaitQueueCond.await();
- } else {
- return m_entryListenerWaitQueueCond.await(
- (long) (timeout * 1e9), TimeUnit.NANOSECONDS);
- }
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- return true;
- }
- }
- }
- } finally {
- m_entryListenerLock.unlock();
- }
- return true;
- }
-
- private final ReentrantLock m_connectionListenerLock = new ReentrantLock();
- private final Map<Integer, Consumer<ConnectionNotification>> m_connectionListeners =
- new HashMap<>();
- private int m_connectionListenerPoller;
- private boolean m_connectionListenerWaitQueue;
- private final Condition m_connectionListenerWaitQueueCond =
- m_connectionListenerLock.newCondition();
-
- @SuppressWarnings("PMD.AvoidCatchingThrowable")
- private void startConnectionListenerThread() {
- var connectionListenerThread =
- new Thread(
- () -> {
- boolean wasInterrupted = false;
- while (!Thread.interrupted()) {
- ConnectionNotification[] events;
- try {
- events =
- NetworkTablesJNI.pollConnectionListener(this, m_connectionListenerPoller);
- } catch (InterruptedException ex) {
- m_connectionListenerLock.lock();
- try {
- if (m_connectionListenerWaitQueue) {
- m_connectionListenerWaitQueue = false;
- m_connectionListenerWaitQueueCond.signalAll();
- continue;
- }
- } finally {
- m_connectionListenerLock.unlock();
- }
- Thread.currentThread().interrupt();
- // don't try to destroy poller, as its handle is likely no longer valid
- wasInterrupted = true;
- break;
- }
- for (ConnectionNotification event : events) {
- Consumer<ConnectionNotification> listener;
- m_connectionListenerLock.lock();
- try {
- listener = m_connectionListeners.get(event.listener);
- } finally {
- m_connectionListenerLock.unlock();
- }
- if (listener != null) {
- try {
- listener.accept(event);
- } catch (Throwable throwable) {
- System.err.println(
- "Unhandled exception during connection listener callback: "
- + throwable.toString());
- throwable.printStackTrace();
- }
- }
- }
- }
- m_connectionListenerLock.lock();
- try {
- if (!wasInterrupted) {
- NetworkTablesJNI.destroyConnectionListenerPoller(m_connectionListenerPoller);
- }
- m_connectionListenerPoller = 0;
- } finally {
- m_connectionListenerLock.unlock();
- }
- },
- "NTConnectionListener");
- connectionListenerThread.setDaemon(true);
- connectionListenerThread.start();
- }
-
- /**
- * Add a connection listener.
- *
- * @param listener Listener to add
- * @param immediateNotify Notify listener of all existing connections
- * @return Listener handle
- */
- public int addConnectionListener(
- Consumer<ConnectionNotification> listener, boolean immediateNotify) {
- m_connectionListenerLock.lock();
- try {
- if (m_connectionListenerPoller == 0) {
- m_connectionListenerPoller = NetworkTablesJNI.createConnectionListenerPoller(m_handle);
- startConnectionListenerThread();
- }
- int handle =
- NetworkTablesJNI.addPolledConnectionListener(m_connectionListenerPoller, immediateNotify);
- m_connectionListeners.put(handle, listener);
- return handle;
- } finally {
- m_connectionListenerLock.unlock();
- }
- }
-
- /**
- * Remove a connection listener.
- *
- * @param listener Listener handle to remove
- */
- public void removeConnectionListener(int listener) {
- m_connectionListenerLock.lock();
- try {
- m_connectionListeners.remove(listener);
- } finally {
- m_connectionListenerLock.unlock();
- }
- NetworkTablesJNI.removeConnectionListener(listener);
- }
-
- /**
- * Wait for the connection listener queue to be empty. This is primarily useful for deterministic
- * testing. This blocks until either the connection listener queue is empty (e.g. there are no
- * more events that need to be passed along to callbacks or poll queues) or the timeout expires.
- *
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
- * block indefinitely
- * @return False if timed out, otherwise true.
- */
- public boolean waitForConnectionListenerQueue(double timeout) {
- if (!NetworkTablesJNI.waitForConnectionListenerQueue(m_handle, timeout)) {
- return false;
- }
- m_connectionListenerLock.lock();
- try {
- if (m_connectionListenerPoller != 0) {
- m_connectionListenerWaitQueue = true;
- NetworkTablesJNI.cancelPollConnectionListener(m_connectionListenerPoller);
- while (m_connectionListenerWaitQueue) {
- try {
- if (timeout < 0) {
- m_connectionListenerWaitQueueCond.await();
- } else {
- return m_connectionListenerWaitQueueCond.await(
- (long) (timeout * 1e9), TimeUnit.NANOSECONDS);
- }
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- return true;
- }
- }
- }
- } finally {
- m_connectionListenerLock.unlock();
- }
- return true;
- }
-
- /*
- * Remote Procedure Call Functions
- */
-
- private final ReentrantLock m_rpcCallLock = new ReentrantLock();
- private final Map<Integer, EntryConsumer<RpcAnswer>> m_rpcCalls = new HashMap<>();
- private int m_rpcCallPoller;
- private boolean m_rpcCallWaitQueue;
- private final Condition m_rpcCallWaitQueueCond = m_rpcCallLock.newCondition();
-
- @SuppressWarnings("PMD.AvoidCatchingThrowable")
- private void startRpcCallThread() {
- var rpcCallThread =
- new Thread(
- () -> {
- boolean wasInterrupted = false;
- while (!Thread.interrupted()) {
- RpcAnswer[] events;
- try {
- events = NetworkTablesJNI.pollRpc(this, m_rpcCallPoller);
- } catch (InterruptedException ex) {
- m_rpcCallLock.lock();
- try {
- if (m_rpcCallWaitQueue) {
- m_rpcCallWaitQueue = false;
- m_rpcCallWaitQueueCond.signalAll();
- continue;
- }
- } finally {
- m_rpcCallLock.unlock();
- }
- Thread.currentThread().interrupt();
- // don't try to destroy poller, as its handle is likely no longer valid
- wasInterrupted = true;
- break;
- }
- for (RpcAnswer event : events) {
- EntryConsumer<RpcAnswer> listener;
- m_rpcCallLock.lock();
- try {
- listener = m_rpcCalls.get(event.entry);
- } finally {
- m_rpcCallLock.unlock();
- }
- if (listener != null) {
- event.m_entryObject = listener.m_entry;
- try {
- listener.m_consumer.accept(event);
- } catch (Throwable throwable) {
- System.err.println(
- "Unhandled exception during RPC callback: " + throwable.toString());
- throwable.printStackTrace();
- }
- event.finish();
- }
- }
- }
- m_rpcCallLock.lock();
- try {
- if (!wasInterrupted) {
- NetworkTablesJNI.destroyRpcCallPoller(m_rpcCallPoller);
- }
- m_rpcCallPoller = 0;
- } finally {
- m_rpcCallLock.unlock();
- }
- },
- "NTRpcCall");
- rpcCallThread.setDaemon(true);
- rpcCallThread.start();
- }
-
- private static final byte[] rev0def = new byte[] {0};
-
- /**
- * Create a callback-based RPC entry point. Only valid to use on the server. The callback function
- * will be called when the RPC is called. This function creates RPC version 0 definitions (raw
- * data in and out).
- *
- * @param entry the entry
- * @param callback callback function
- */
- public void createRpc(NetworkTableEntry entry, Consumer<RpcAnswer> callback) {
- m_rpcCallLock.lock();
- try {
- if (m_rpcCallPoller == 0) {
- m_rpcCallPoller = NetworkTablesJNI.createRpcCallPoller(m_handle);
- startRpcCallThread();
- }
- NetworkTablesJNI.createPolledRpc(entry.getHandle(), rev0def, m_rpcCallPoller);
- m_rpcCalls.put(entry.getHandle(), new EntryConsumer<>(entry, callback));
- } finally {
- m_rpcCallLock.unlock();
- }
- }
-
- /**
- * Wait for the incoming RPC call queue to be empty. This is primarily useful for deterministic
- * testing. This blocks until either the RPC call queue is empty (e.g. there are no more events
- * that need to be passed along to callbacks or poll queues) or the timeout expires.
- *
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
- * block indefinitely
- * @return False if timed out, otherwise true.
- */
- public boolean waitForRpcCallQueue(double timeout) {
- if (!NetworkTablesJNI.waitForRpcCallQueue(m_handle, timeout)) {
- return false;
- }
- m_rpcCallLock.lock();
- try {
- if (m_rpcCallPoller != 0) {
- m_rpcCallWaitQueue = true;
- NetworkTablesJNI.cancelPollRpc(m_rpcCallPoller);
- while (m_rpcCallWaitQueue) {
- try {
- if (timeout < 0) {
- m_rpcCallWaitQueueCond.await();
- } else {
- return m_rpcCallWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
- }
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- return true;
- }
- }
- }
- } finally {
- m_rpcCallLock.unlock();
- }
- return true;
- }
-
- /*
- * Client/Server Functions
- */
-
- /**
- * Set the network identity of this node. This is the name used during the initial connection
- * handshake, and is visible through ConnectionInfo on the remote node.
- *
- * @param name identity to advertise
- */
- public void setNetworkIdentity(String name) {
- NetworkTablesJNI.setNetworkIdentity(m_handle, name);
- }
-
- /**
- * Get the current network mode.
- *
- * @return Bitmask of NetworkMode.
- */
- public int getNetworkMode() {
- return NetworkTablesJNI.getNetworkMode(m_handle);
- }
-
- /**
- * Starts local-only operation. Prevents calls to startServer or startClient from taking effect.
- * Has no effect if startServer or startClient has already been called.
- */
- public void startLocal() {
- NetworkTablesJNI.startLocal(m_handle);
- }
-
- /**
- * Stops local-only operation. startServer or startClient can be called after this call to start a
- * server or client.
- */
- public void stopLocal() {
- NetworkTablesJNI.stopLocal(m_handle);
- }
-
- /**
- * Starts a server using the networktables.ini as the persistent file, using the default listening
- * address and port.
- */
- public void startServer() {
- startServer("networktables.ini");
- }
-
- /**
- * Starts a server using the specified persistent filename, using the default listening address
- * and port.
- *
- * @param persistFilename the name of the persist file to use
- */
- public void startServer(String persistFilename) {
- startServer(persistFilename, "");
- }
-
- /**
- * Starts a server using the specified filename and listening address, using the default port.
- *
- * @param persistFilename the name of the persist file to use
- * @param listenAddress the address to listen on, or empty to listen on any address
- */
- public void startServer(String persistFilename, String listenAddress) {
- startServer(persistFilename, listenAddress, kDefaultPort);
- }
-
- /**
- * Starts a server using the specified filename, listening address, and port.
- *
- * @param persistFilename the name of the persist file to use
- * @param listenAddress the address to listen on, or empty to listen on any address
- * @param port port to communicate over
- */
- public void startServer(String persistFilename, String listenAddress, int port) {
- NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port);
- }
-
- /** Stops the server if it is running. */
- public void stopServer() {
- NetworkTablesJNI.stopServer(m_handle);
- }
-
- /** Starts a client. Use SetServer to set the server name and port. */
- public void startClient() {
- NetworkTablesJNI.startClient(m_handle);
- }
-
- /**
- * Starts a client using the specified server and the default port.
- *
- * @param serverName server name
- */
- public void startClient(String serverName) {
- startClient(serverName, kDefaultPort);
- }
-
- /**
- * Starts a client using the specified server and port.
- *
- * @param serverName server name
- * @param port port to communicate over
- */
- public void startClient(String serverName, int port) {
- NetworkTablesJNI.startClient(m_handle, serverName, port);
- }
-
- /**
- * Starts a client using the specified servers and default port. The client will attempt to
- * connect to each server in round robin fashion.
- *
- * @param serverNames array of server names
- */
- public void startClient(String[] serverNames) {
- startClient(serverNames, kDefaultPort);
- }
-
- /**
- * Starts a client using the specified servers and port number. The client will attempt to connect
- * to each server in round robin fashion.
- *
- * @param serverNames array of server names
- * @param port port to communicate over
- */
- public void startClient(String[] serverNames, int port) {
- int[] ports = new int[serverNames.length];
- for (int i = 0; i < serverNames.length; i++) {
- ports[i] = port;
- }
- startClient(serverNames, ports);
- }
-
- /**
- * Starts a client using the specified (server, port) combinations. The client will attempt to
- * connect to each server in round robin fashion.
- *
- * @param serverNames array of server names
- * @param ports array of port numbers
- */
- public void startClient(String[] serverNames, int[] ports) {
- NetworkTablesJNI.startClient(m_handle, serverNames, ports);
- }
-
- /**
- * Starts a client using commonly known robot addresses for the specified team using the default
- * port number.
- *
- * @param team team number
- */
- public void startClientTeam(int team) {
- startClientTeam(team, kDefaultPort);
- }
-
- /**
- * Starts a client using commonly known robot addresses for the specified team.
- *
- * @param team team number
- * @param port port to communicate over
- */
- public void startClientTeam(int team, int port) {
- NetworkTablesJNI.startClientTeam(m_handle, team, port);
- }
-
- /** Stops the client if it is running. */
- public void stopClient() {
- NetworkTablesJNI.stopClient(m_handle);
- }
-
- /**
- * Sets server address and port for client (without restarting client). Changes the port to the
- * default port.
- *
- * @param serverName server name
- */
- public void setServer(String serverName) {
- setServer(serverName, kDefaultPort);
- }
-
- /**
- * Sets server address and port for client (without restarting client).
- *
- * @param serverName server name
- * @param port port to communicate over
- */
- public void setServer(String serverName, int port) {
- NetworkTablesJNI.setServer(m_handle, serverName, port);
- }
-
- /**
- * Sets server addresses and port for client (without restarting client). Changes the port to the
- * default port. The client will attempt to connect to each server in round robin fashion.
- *
- * @param serverNames array of server names
- */
- public void setServer(String[] serverNames) {
- setServer(serverNames, kDefaultPort);
- }
-
- /**
- * Sets server addresses and port for client (without restarting client). The client will attempt
- * to connect to each server in round robin fashion.
- *
- * @param serverNames array of server names
- * @param port port to communicate over
- */
- public void setServer(String[] serverNames, int port) {
- int[] ports = new int[serverNames.length];
- for (int i = 0; i < serverNames.length; i++) {
- ports[i] = port;
- }
- setServer(serverNames, ports);
- }
-
- /**
- * Sets server addresses and ports for client (without restarting client). The client will attempt
- * to connect to each server in round robin fashion.
- *
- * @param serverNames array of server names
- * @param ports array of port numbers
- */
- public void setServer(String[] serverNames, int[] ports) {
- NetworkTablesJNI.setServer(m_handle, serverNames, ports);
- }
-
- /**
- * Sets server addresses and port for client (without restarting client). Changes the port to the
- * default port. The client will attempt to connect to each server in round robin fashion.
- *
- * @param team team number
- */
- public void setServerTeam(int team) {
- setServerTeam(team, kDefaultPort);
- }
-
- /**
- * Sets server addresses and port for client (without restarting client). Connects using commonly
- * known robot addresses for the specified team.
- *
- * @param team team number
- * @param port port to communicate over
- */
- public void setServerTeam(int team, int port) {
- NetworkTablesJNI.setServerTeam(m_handle, team, port);
- }
-
- /**
- * Starts requesting server address from Driver Station. This connects to the Driver Station
- * running on localhost to obtain the server IP address, and connects with the default port.
- */
- public void startDSClient() {
- startDSClient(kDefaultPort);
- }
-
- /**
- * Starts requesting server address from Driver Station. This connects to the Driver Station
- * running on localhost to obtain the server IP address.
- *
- * @param port server port to use in combination with IP from DS
- */
- public void startDSClient(int port) {
- NetworkTablesJNI.startDSClient(m_handle, port);
- }
-
- /** Stops requesting server address from Driver Station. */
- public void stopDSClient() {
- NetworkTablesJNI.stopDSClient(m_handle);
- }
-
- /**
- * Set the periodic update rate. Sets how frequently updates are sent to other nodes over the
- * network.
- *
- * @param interval update interval in seconds (range 0.01 to 1.0)
- */
- public void setUpdateRate(double interval) {
- NetworkTablesJNI.setUpdateRate(m_handle, interval);
- }
-
- /**
- * Flushes all updated values immediately to the network. Note: This is rate-limited to protect
- * the network from flooding. This is primarily useful for synchronizing network updates with user
- * code.
- */
- public void flush() {
- NetworkTablesJNI.flush(m_handle);
- }
-
- /**
- * Gets information on the currently established network connections. If operating as a client,
- * this will return either zero or one values.
- *
- * @return array of connection information
- */
- public ConnectionInfo[] getConnections() {
- return NetworkTablesJNI.getConnections(m_handle);
- }
-
- /**
- * Return whether or not the instance is connected to another node.
- *
- * @return True if connected.
- */
- public boolean isConnected() {
- return NetworkTablesJNI.isConnected(m_handle);
- }
-
- /**
- * Saves persistent keys to a file. The server does this automatically.
- *
- * @param filename file name
- * @throws PersistentException if error saving file
- */
- public void savePersistent(String filename) throws PersistentException {
- NetworkTablesJNI.savePersistent(m_handle, filename);
- }
-
- /**
- * Loads persistent keys from a file. The server does this automatically.
- *
- * @param filename file name
- * @return List of warnings (errors result in an exception instead)
- * @throws PersistentException if error reading file
- */
- public String[] loadPersistent(String filename) throws PersistentException {
- return NetworkTablesJNI.loadPersistent(m_handle, filename);
- }
-
- /**
- * Save table values to a file. The file format used is identical to that used for SavePersistent.
- *
- * @param filename filename
- * @param prefix save only keys starting with this prefix
- * @throws PersistentException if error saving file
- */
- public void saveEntries(String filename, String prefix) throws PersistentException {
- NetworkTablesJNI.saveEntries(m_handle, filename, prefix);
- }
-
- /**
- * Load table values from a file. The file format used is identical to that used for
- * SavePersistent / LoadPersistent.
- *
- * @param filename filename
- * @param prefix load only keys starting with this prefix
- * @return List of warnings (errors result in an exception instead)
- * @throws PersistentException if error saving file
- */
- public String[] loadEntries(String filename, String prefix) throws PersistentException {
- return NetworkTablesJNI.loadEntries(m_handle, filename, prefix);
- }
-
- private final ReentrantLock m_loggerLock = new ReentrantLock();
- private final Map<Integer, Consumer<LogMessage>> m_loggers = new HashMap<>();
- private int m_loggerPoller;
- private boolean m_loggerWaitQueue;
- private final Condition m_loggerWaitQueueCond = m_loggerLock.newCondition();
-
- @SuppressWarnings("PMD.AvoidCatchingThrowable")
- private void startLogThread() {
- var loggerThread =
- new Thread(
- () -> {
- boolean wasInterrupted = false;
- while (!Thread.interrupted()) {
- LogMessage[] events;
- try {
- events = NetworkTablesJNI.pollLogger(this, m_loggerPoller);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- // don't try to destroy poller, as its handle is likely no longer valid
- wasInterrupted = true;
- break;
- }
- for (LogMessage event : events) {
- Consumer<LogMessage> logger;
- m_loggerLock.lock();
- try {
- logger = m_loggers.get(event.logger);
- } finally {
- m_loggerLock.unlock();
- }
- if (logger != null) {
- try {
- logger.accept(event);
- } catch (Throwable throwable) {
- System.err.println(
- "Unhandled exception during logger callback: " + throwable.toString());
- throwable.printStackTrace();
- }
- }
- }
- }
- m_loggerLock.lock();
- try {
- if (!wasInterrupted) {
- NetworkTablesJNI.destroyLoggerPoller(m_loggerPoller);
- }
- m_rpcCallPoller = 0;
- } finally {
- m_loggerLock.unlock();
- }
- },
- "NTLogger");
- loggerThread.setDaemon(true);
- loggerThread.start();
- }
-
- /**
- * Add logger callback function. By default, log messages are sent to stderr; this function sends
- * log messages with the specified levels to the provided callback function instead. The callback
- * function will only be called for log messages with level greater than or equal to minLevel and
- * less than or equal to maxLevel; messages outside this range will be silently ignored.
- *
- * @param func log callback function
- * @param minLevel minimum log level
- * @param maxLevel maximum log level
- * @return Logger handle
- */
- public int addLogger(Consumer<LogMessage> func, int minLevel, int maxLevel) {
- m_loggerLock.lock();
- try {
- if (m_loggerPoller == 0) {
- m_loggerPoller = NetworkTablesJNI.createLoggerPoller(m_handle);
- startLogThread();
- }
- int handle = NetworkTablesJNI.addPolledLogger(m_loggerPoller, minLevel, maxLevel);
- m_loggers.put(handle, func);
- return handle;
- } finally {
- m_loggerLock.unlock();
- }
- }
-
- /**
- * Remove a logger.
- *
- * @param logger Logger handle to remove
- */
- public void removeLogger(int logger) {
- m_loggerLock.lock();
- try {
- m_loggers.remove(logger);
- } finally {
- m_loggerLock.unlock();
- }
- NetworkTablesJNI.removeLogger(logger);
- }
-
- /**
- * Wait for the incoming log event queue to be empty. This is primarily useful for deterministic
- * testing. This blocks until either the log event queue is empty (e.g. there are no more events
- * that need to be passed along to callbacks or poll queues) or the timeout expires.
- *
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
- * block indefinitely
- * @return False if timed out, otherwise true.
- */
- public boolean waitForLoggerQueue(double timeout) {
- if (!NetworkTablesJNI.waitForLoggerQueue(m_handle, timeout)) {
- return false;
- }
- m_loggerLock.lock();
- try {
- if (m_loggerPoller != 0) {
- m_loggerWaitQueue = true;
- NetworkTablesJNI.cancelPollLogger(m_loggerPoller);
- while (m_loggerWaitQueue) {
- try {
- if (timeout < 0) {
- m_loggerWaitQueueCond.await();
- } else {
- return m_loggerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
- }
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- return true;
- }
- }
- }
- } finally {
- m_loggerLock.unlock();
- }
- return true;
- }
-
- @Override
- public boolean equals(Object other) {
- if (other == this) {
- return true;
- }
- if (!(other instanceof NetworkTableInstance)) {
- return false;
- }
-
- return m_handle == ((NetworkTableInstance) other).m_handle;
- }
-
- @Override
- public int hashCode() {
- return m_handle;
- }
-
- private boolean m_owned;
- private final int m_handle;
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListener.java
new file mode 100644
index 0000000..2568738
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListener.java
@@ -0,0 +1,189 @@
+// 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.EnumSet;
+import java.util.function.Consumer;
+
+/**
+ * Event listener. This calls back to a callback function when an event matching the specified mask
+ * occurs. The callback function is called asynchronously on a separate thread, so it's important to
+ * use synchronization or atomics when accessing any shared state from the callback function.
+ */
+public final class NetworkTableListener implements AutoCloseable {
+ /**
+ * Create a listener for changes to topics with names that start with any of the given prefixes.
+ * This creates a corresponding internal subscriber with the lifetime of the listener.
+ *
+ * @param inst Instance
+ * @param prefixes Topic name string prefixes
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createListener(
+ NetworkTableInstance inst,
+ String[] prefixes,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ return new NetworkTableListener(inst, inst.addListener(prefixes, eventKinds, listener));
+ }
+
+ /**
+ * Create a listener for changes on a particular topic. This creates a corresponding internal
+ * subscriber with the lifetime of the listener.
+ *
+ * @param topic Topic
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createListener(
+ Topic topic,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ NetworkTableInstance inst = topic.getInstance();
+ return new NetworkTableListener(inst, inst.addListener(topic, eventKinds, listener));
+ }
+
+ /**
+ * Create a listener for topic changes on a subscriber. This does NOT keep the subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createListener(
+ Subscriber subscriber,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ NetworkTableInstance inst = subscriber.getTopic().getInstance();
+ return new NetworkTableListener(inst, inst.addListener(subscriber, eventKinds, listener));
+ }
+
+ /**
+ * Create a listener for topic changes on a subscriber. This does NOT keep the subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createListener(
+ MultiSubscriber subscriber,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ NetworkTableInstance inst = subscriber.getInstance();
+ return new NetworkTableListener(inst, inst.addListener(subscriber, eventKinds, listener));
+ }
+
+ /**
+ * Create a listener for topic changes on an entry.
+ *
+ * @param entry Entry
+ * @param eventKinds set of event kinds to listen to
+ * @param listener Listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createListener(
+ NetworkTableEntry entry,
+ EnumSet<NetworkTableEvent.Kind> eventKinds,
+ Consumer<NetworkTableEvent> listener) {
+ NetworkTableInstance inst = entry.getInstance();
+ return new NetworkTableListener(inst, inst.addListener(entry, eventKinds, listener));
+ }
+
+ /**
+ * Create a connection listener.
+ *
+ * @param inst instance
+ * @param immediateNotify notify listener of all existing connections
+ * @param listener listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createConnectionListener(
+ NetworkTableInstance inst, boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
+ return new NetworkTableListener(inst, inst.addConnectionListener(immediateNotify, listener));
+ }
+
+ /**
+ * Create a time synchronization listener.
+ *
+ * @param inst instance
+ * @param immediateNotify notify listener of current time synchonization value
+ * @param listener listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createTimeSyncListener(
+ NetworkTableInstance inst, boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
+ return new NetworkTableListener(inst, inst.addTimeSyncListener(immediateNotify, listener));
+ }
+
+ /**
+ * Create a listener for log messages. By default, log messages are sent to stderr; this function
+ * sends log messages with the specified levels to the provided callback function instead. The
+ * callback function will only be called for log messages with level greater than or equal to
+ * minLevel and less than or equal to maxLevel; messages outside this range will be silently
+ * ignored.
+ *
+ * @param inst instance
+ * @param minLevel minimum log level
+ * @param maxLevel maximum log level
+ * @param listener listener function
+ * @return Listener
+ */
+ public static NetworkTableListener createLogger(
+ NetworkTableInstance inst, int minLevel, int maxLevel, Consumer<NetworkTableEvent> listener) {
+ return new NetworkTableListener(inst, inst.addLogger(minLevel, maxLevel, listener));
+ }
+
+ @Override
+ public synchronized void close() {
+ if (m_handle != 0) {
+ m_inst.removeListener(m_handle);
+ m_handle = 0;
+ }
+ }
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ public boolean isValid() {
+ return m_handle != 0;
+ }
+
+ /**
+ * Gets the native handle.
+ *
+ * @return Native handle
+ */
+ public int getHandle() {
+ return m_handle;
+ }
+
+ /**
+ * Wait for the topic listener queue to be empty. This is primarily useful for deterministic
+ * testing. This blocks until either the topic listener queue is empty (e.g. there are no more
+ * events that need to be passed along to callbacks or poll queues) or the timeout expires.
+ *
+ * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
+ * block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+ public boolean waitForQueue(double timeout) {
+ return m_inst.waitForListenerQueue(timeout);
+ }
+
+ private NetworkTableListener(NetworkTableInstance inst, int handle) {
+ m_inst = inst;
+ m_handle = handle;
+ }
+
+ private final NetworkTableInstance m_inst;
+ private int m_handle;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListenerPoller.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListenerPoller.java
new file mode 100644
index 0000000..3a504f3
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableListenerPoller.java
@@ -0,0 +1,173 @@
+// 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.EnumSet;
+
+/**
+ * Event listener poller. This queues events matching the specified mask. Code using the listener
+ * must periodically call readQueue() to read the events.
+ */
+public final class NetworkTableListenerPoller implements AutoCloseable {
+ /**
+ * Construct a topic listener poller.
+ *
+ * @param inst Instance
+ */
+ public NetworkTableListenerPoller(NetworkTableInstance inst) {
+ m_inst = inst;
+ m_handle = NetworkTablesJNI.createListenerPoller(inst.getHandle());
+ }
+
+ /**
+ * Start listening to topic changes for topics with names that start with any of the given
+ * prefixes. This creates a corresponding internal subscriber with the lifetime of the listener.
+ *
+ * @param prefixes Topic name string prefixes
+ * @param eventKinds set of event kinds to listen to
+ * @return Listener handle
+ */
+ public int addListener(String[] prefixes, EnumSet<NetworkTableEvent.Kind> eventKinds) {
+ return NetworkTablesJNI.addListener(m_handle, prefixes, eventKinds);
+ }
+
+ /**
+ * Start listening to changes to a particular topic. This creates a corresponding internal
+ * subscriber with the lifetime of the listener.
+ *
+ * @param topic Topic
+ * @param eventKinds set of event kinds to listen to
+ * @return Listener handle
+ */
+ public int addListener(Topic topic, EnumSet<NetworkTableEvent.Kind> eventKinds) {
+ return NetworkTablesJNI.addListener(m_handle, topic.getHandle(), eventKinds);
+ }
+
+ /**
+ * Start listening to topic changes on a subscriber. This does NOT keep the subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param eventKinds set of event kinds to listen to
+ * @return Listener handle
+ */
+ public int addListener(Subscriber subscriber, EnumSet<NetworkTableEvent.Kind> eventKinds) {
+ return NetworkTablesJNI.addListener(m_handle, subscriber.getHandle(), eventKinds);
+ }
+
+ /**
+ * Start listening to topic changes on a subscriber. This does NOT keep the subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param eventKinds set of event kinds to listen to
+ * @return Listener handle
+ */
+ public int addListener(MultiSubscriber subscriber, EnumSet<NetworkTableEvent.Kind> eventKinds) {
+ return NetworkTablesJNI.addListener(m_handle, subscriber.getHandle(), eventKinds);
+ }
+
+ /**
+ * Start listening to topic changes on an entry.
+ *
+ * @param entry Entry
+ * @param eventKinds set of event kinds to listen to
+ * @return Listener handle
+ */
+ public int addListener(NetworkTableEntry entry, EnumSet<NetworkTableEvent.Kind> eventKinds) {
+ return NetworkTablesJNI.addListener(m_handle, entry.getHandle(), eventKinds);
+ }
+
+ /**
+ * Add a connection listener. The callback function is called asynchronously on a separate thread,
+ * so it's important to use synchronization or atomics when accessing any shared state from the
+ * callback function.
+ *
+ * @param immediateNotify notify listener of all existing connections
+ * @return Listener handle
+ */
+ public int addConnectionListener(boolean immediateNotify) {
+ EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kConnection);
+ if (immediateNotify) {
+ eventKinds.add(NetworkTableEvent.Kind.kImmediate);
+ }
+ return NetworkTablesJNI.addListener(m_handle, m_inst.getHandle(), eventKinds);
+ }
+
+ /**
+ * Add a time synchronization listener. The callback function is called asynchronously on a
+ * separate thread, so it's important to use synchronization or atomics when accessing any shared
+ * state from the callback function.
+ *
+ * @param immediateNotify notify listener of current time synchronization value
+ * @return Listener handle
+ */
+ public int addTimeSyncListener(boolean immediateNotify) {
+ EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kTimeSync);
+ if (immediateNotify) {
+ eventKinds.add(NetworkTableEvent.Kind.kImmediate);
+ }
+ return NetworkTablesJNI.addListener(m_handle, m_inst.getHandle(), eventKinds);
+ }
+
+ /**
+ * Add logger callback function. By default, log messages are sent to stderr; this function sends
+ * log messages with the specified levels to the provided callback function instead. The callback
+ * function will only be called for log messages with level greater than or equal to minLevel and
+ * less than or equal to maxLevel; messages outside this range will be silently ignored.
+ *
+ * @param minLevel minimum log level
+ * @param maxLevel maximum log level
+ * @return Listener handle
+ */
+ public int addLogger(int minLevel, int maxLevel) {
+ return NetworkTablesJNI.addLogger(m_handle, minLevel, maxLevel);
+ }
+
+ /**
+ * Remove a listener.
+ *
+ * @param listener Listener handle
+ */
+ public void removeListener(int listener) {
+ NetworkTablesJNI.removeListener(listener);
+ }
+
+ /**
+ * Read topic notifications.
+ *
+ * @return Topic notifications since the previous call to readQueue()
+ */
+ public NetworkTableEvent[] readQueue() {
+ return NetworkTablesJNI.readListenerQueue(m_inst, m_handle);
+ }
+
+ @Override
+ public synchronized void close() {
+ if (m_handle != 0) {
+ NetworkTablesJNI.destroyListenerPoller(m_handle);
+ }
+ m_handle = 0;
+ }
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ public boolean isValid() {
+ return m_handle != 0;
+ }
+
+ /**
+ * Gets the native handle.
+ *
+ * @return Handle
+ */
+ public int getHandle() {
+ return m_handle;
+ }
+
+ private final NetworkTableInstance m_inst;
+ private int m_handle;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
index a173bb2..2350b49 100644
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java
@@ -6,24 +6,33 @@
/** Network table data types. */
public enum NetworkTableType {
- kUnassigned(0),
- kBoolean(0x01),
- kDouble(0x02),
- kString(0x04),
- kRaw(0x08),
- kBooleanArray(0x10),
- kDoubleArray(0x20),
- kStringArray(0x40),
- kRpc(0x80);
+ kUnassigned(0, ""),
+ kBoolean(0x01, "boolean"),
+ kDouble(0x02, "double"),
+ kString(0x04, "string"),
+ kRaw(0x08, "raw"),
+ kBooleanArray(0x10, "boolean[]"),
+ kDoubleArray(0x20, "double[]"),
+ kStringArray(0x40, "string[]"),
+ kInteger(0x100, "int"),
+ kFloat(0x200, "float"),
+ kIntegerArray(0x400, "int[]"),
+ kFloatArray(0x800, "float[]");
- private final int value;
+ private final int m_value;
+ private final String m_valueStr;
- NetworkTableType(int value) {
- this.value = value;
+ NetworkTableType(int value, String valueStr) {
+ m_value = value;
+ m_valueStr = valueStr;
}
public int getValue() {
- return value;
+ return m_value;
+ }
+
+ public String getValueStr() {
+ return m_valueStr;
}
/**
@@ -48,10 +57,89 @@
return kDoubleArray;
case 0x40:
return kStringArray;
- case 0x80:
- return kRpc;
+ case 0x100:
+ return kInteger;
+ case 0x200:
+ return kFloat;
+ case 0x400:
+ return kIntegerArray;
+ case 0x800:
+ return kFloatArray;
default:
return kUnassigned;
}
}
+
+ /**
+ * Convert from a type string to an enum type.
+ *
+ * @param typeString type string
+ * @return The kind
+ */
+ public static NetworkTableType getFromString(String typeString) {
+ switch (typeString) {
+ case "boolean":
+ return kBoolean;
+ case "double":
+ return kDouble;
+ case "float":
+ return kFloat;
+ case "int":
+ return kInteger;
+ case "string":
+ case "json":
+ return kString;
+ case "boolean[]":
+ return kBooleanArray;
+ case "double[]":
+ return kDoubleArray;
+ case "float[]":
+ return kFloatArray;
+ case "int[]":
+ return kIntegerArray;
+ case "string[]":
+ return kStringArray;
+ case "":
+ return kUnassigned;
+ default:
+ return kRaw;
+ }
+ }
+
+ /**
+ * Gets string from generic data value.
+ *
+ * @param data the data to check
+ * @return type string of the data, or empty string if no match
+ */
+ public static String getStringFromObject(Object data) {
+ if (data instanceof Boolean) {
+ return "boolean";
+ } else if (data instanceof Float) {
+ return "float";
+ } else if (data instanceof Long) {
+ // Checking Long because NT supports 64-bit integers
+ return "int";
+ } else if (data instanceof Double || data instanceof Number) {
+ // If typeof Number class, return "double" as the type. Functions as a "catch-all".
+ return "double";
+ } else if (data instanceof String) {
+ return "string";
+ } else if (data instanceof boolean[] || data instanceof Boolean[]) {
+ return "boolean[]";
+ } else if (data instanceof float[] || data instanceof Float[]) {
+ return "float[]";
+ } else if (data instanceof long[] || data instanceof Long[]) {
+ return "int[]";
+ } else if (data instanceof double[] || data instanceof Double[] || data instanceof Number[]) {
+ // If typeof Number class, return "double[]" as the type. Functions as a "catch-all".
+ return "double[]";
+ } else if (data instanceof String[]) {
+ return "string[]";
+ } else if (data instanceof byte[] || data instanceof Byte[]) {
+ return "raw";
+ } else {
+ return "";
+ }
+ }
}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java
deleted file mode 100644
index f4b9290..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java
+++ /dev/null
@@ -1,522 +0,0 @@
-// 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.Objects;
-
-/** A network table entry value. */
-public final class NetworkTableValue {
- NetworkTableValue(NetworkTableType type, Object value, long time) {
- m_type = type;
- m_value = value;
- m_time = time;
- }
-
- NetworkTableValue(NetworkTableType type, Object value) {
- this(type, value, NetworkTablesJNI.now());
- }
-
- NetworkTableValue(int type, Object value, long time) {
- this(NetworkTableType.getFromInt(type), value, time);
- }
-
- /**
- * Get the data type.
- *
- * @return The type.
- */
- public NetworkTableType getType() {
- return m_type;
- }
-
- /**
- * Get the data value stored.
- *
- * @return The type.
- */
- public Object getValue() {
- return m_value;
- }
-
- /**
- * Get the creation time of the value.
- *
- * @return The time, in the units returned by NetworkTablesJNI.now().
- */
- public long getTime() {
- return m_time;
- }
-
- /*
- * Type Checkers
- */
-
- /**
- * Determine if entry value contains a value or is unassigned.
- *
- * @return True if the entry value contains a value.
- */
- public boolean isValid() {
- return m_type != NetworkTableType.kUnassigned;
- }
-
- /**
- * Determine if entry value contains a boolean.
- *
- * @return True if the entry value is of boolean type.
- */
- public boolean isBoolean() {
- return m_type == NetworkTableType.kBoolean;
- }
-
- /**
- * Determine if entry value contains a double.
- *
- * @return True if the entry value is of double type.
- */
- public boolean isDouble() {
- return m_type == NetworkTableType.kDouble;
- }
-
- /**
- * Determine if entry value contains a string.
- *
- * @return True if the entry value is of string type.
- */
- public boolean isString() {
- return m_type == NetworkTableType.kString;
- }
-
- /**
- * Determine if entry value contains a raw.
- *
- * @return True if the entry value is of raw type.
- */
- public boolean isRaw() {
- return m_type == NetworkTableType.kRaw;
- }
-
- /**
- * Determine if entry value contains a rpc definition.
- *
- * @return True if the entry value is of rpc definition type.
- */
- public boolean isRpc() {
- return m_type == NetworkTableType.kRpc;
- }
-
- /**
- * Determine if entry value contains a boolean array.
- *
- * @return True if the entry value is of boolean array type.
- */
- public boolean isBooleanArray() {
- return m_type == NetworkTableType.kBooleanArray;
- }
-
- /**
- * Determine if entry value contains a double array.
- *
- * @return True if the entry value is of double array type.
- */
- public boolean isDoubleArray() {
- return m_type == NetworkTableType.kDoubleArray;
- }
-
- /**
- * Determine if entry value contains a string array.
- *
- * @return True if the entry value is of string array type.
- */
- public boolean isStringArray() {
- return m_type == NetworkTableType.kStringArray;
- }
-
- /*
- * Type-Safe Getters
- */
-
- /**
- * Get the entry's boolean value.
- *
- * @return The boolean value.
- * @throws ClassCastException if the entry value is not of boolean type.
- */
- public boolean getBoolean() {
- if (m_type != NetworkTableType.kBoolean) {
- throw new ClassCastException("cannot convert " + m_type + " to boolean");
- }
- return (Boolean) m_value;
- }
-
- /**
- * Get the entry's double value.
- *
- * @return The double value.
- * @throws ClassCastException if the entry value is not of double type.
- */
- public double getDouble() {
- if (m_type != NetworkTableType.kDouble) {
- throw new ClassCastException("cannot convert " + m_type + " to double");
- }
- return ((Number) m_value).doubleValue();
- }
-
- /**
- * Get the entry's string value.
- *
- * @return The string value.
- * @throws ClassCastException if the entry value is not of string type.
- */
- public String getString() {
- if (m_type != NetworkTableType.kString) {
- throw new ClassCastException("cannot convert " + m_type + " to string");
- }
- return (String) m_value;
- }
-
- /**
- * Get the entry's raw value.
- *
- * @return The raw value.
- * @throws ClassCastException if the entry value is not of raw type.
- */
- @SuppressWarnings("PMD.MethodReturnsInternalArray")
- public byte[] getRaw() {
- if (m_type != NetworkTableType.kRaw) {
- throw new ClassCastException("cannot convert " + m_type + " to raw");
- }
- return (byte[]) m_value;
- }
-
- /**
- * Get the entry's rpc definition value.
- *
- * @return The rpc definition value.
- * @throws ClassCastException if the entry value is not of rpc definition type.
- */
- @SuppressWarnings("PMD.MethodReturnsInternalArray")
- public byte[] getRpc() {
- if (m_type != NetworkTableType.kRpc) {
- throw new ClassCastException("cannot convert " + m_type + " to rpc");
- }
- return (byte[]) m_value;
- }
-
- /**
- * Get the entry's boolean array value.
- *
- * @return The boolean array value.
- * @throws ClassCastException if the entry value is not of boolean array type.
- */
- @SuppressWarnings("PMD.MethodReturnsInternalArray")
- public boolean[] getBooleanArray() {
- if (m_type != NetworkTableType.kBooleanArray) {
- throw new ClassCastException("cannot convert " + m_type + " to boolean array");
- }
- return (boolean[]) m_value;
- }
-
- /**
- * Get the entry's double array value.
- *
- * @return The double array value.
- * @throws ClassCastException if the entry value is not of double array type.
- */
- @SuppressWarnings("PMD.MethodReturnsInternalArray")
- public double[] getDoubleArray() {
- if (m_type != NetworkTableType.kDoubleArray) {
- throw new ClassCastException("cannot convert " + m_type + " to double array");
- }
- return (double[]) m_value;
- }
-
- /**
- * Get the entry's string array value.
- *
- * @return The string array value.
- * @throws ClassCastException if the entry value is not of string array type.
- */
- @SuppressWarnings("PMD.MethodReturnsInternalArray")
- public String[] getStringArray() {
- if (m_type != NetworkTableType.kStringArray) {
- throw new ClassCastException("cannot convert " + m_type + " to string array");
- }
- return (String[]) m_value;
- }
-
- /*
- * Factory functions.
- */
-
- /**
- * Creates a boolean entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeBoolean(boolean value) {
- return new NetworkTableValue(NetworkTableType.kBoolean, Boolean.valueOf(value));
- }
-
- /**
- * Creates a boolean entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeBoolean(boolean value, long time) {
- return new NetworkTableValue(NetworkTableType.kBoolean, Boolean.valueOf(value), time);
- }
-
- /**
- * Creates a double entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeDouble(double value) {
- return new NetworkTableValue(NetworkTableType.kDouble, Double.valueOf(value));
- }
-
- /**
- * Creates a double entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeDouble(double value, long time) {
- return new NetworkTableValue(NetworkTableType.kDouble, Double.valueOf(value), time);
- }
-
- /**
- * Creates a string entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeString(String value) {
- return new NetworkTableValue(NetworkTableType.kString, value);
- }
-
- /**
- * Creates a string entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeString(String value, long time) {
- return new NetworkTableValue(NetworkTableType.kString, value, time);
- }
-
- /**
- * Creates a raw entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeRaw(byte[] value) {
- return new NetworkTableValue(NetworkTableType.kRaw, value);
- }
-
- /**
- * Creates a raw entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeRaw(byte[] value, long time) {
- return new NetworkTableValue(NetworkTableType.kRaw, value, time);
- }
-
- /**
- * Creates a rpc entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeRpc(byte[] value) {
- return new NetworkTableValue(NetworkTableType.kRpc, value);
- }
-
- /**
- * Creates a rpc entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeRpc(byte[] value, long time) {
- return new NetworkTableValue(NetworkTableType.kRpc, value, time);
- }
-
- /**
- * Creates a boolean array entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeBooleanArray(boolean[] value) {
- return new NetworkTableValue(NetworkTableType.kBooleanArray, value);
- }
-
- /**
- * Creates a boolean array entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeBooleanArray(boolean[] value, long time) {
- return new NetworkTableValue(NetworkTableType.kBooleanArray, value, time);
- }
-
- /**
- * Creates a boolean array entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeBooleanArray(Boolean[] value) {
- return new NetworkTableValue(NetworkTableType.kBooleanArray, toNative(value));
- }
-
- /**
- * Creates a boolean array entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeBooleanArray(Boolean[] value, long time) {
- return new NetworkTableValue(NetworkTableType.kBooleanArray, toNative(value), time);
- }
-
- /**
- * Creates a double array entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeDoubleArray(double[] value) {
- return new NetworkTableValue(NetworkTableType.kDoubleArray, value);
- }
-
- /**
- * Creates a double array entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeDoubleArray(double[] value, long time) {
- return new NetworkTableValue(NetworkTableType.kDoubleArray, value, time);
- }
-
- /**
- * Creates a double array entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeDoubleArray(Number[] value) {
- return new NetworkTableValue(NetworkTableType.kDoubleArray, toNative(value));
- }
-
- /**
- * Creates a double array entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeDoubleArray(Number[] value, long time) {
- return new NetworkTableValue(NetworkTableType.kDoubleArray, toNative(value), time);
- }
-
- /**
- * Creates a string array entry value.
- *
- * @param value the value
- * @return The entry value
- */
- public static NetworkTableValue makeStringArray(String[] value) {
- return new NetworkTableValue(NetworkTableType.kStringArray, value);
- }
-
- /**
- * Creates a string array entry value.
- *
- * @param value the value
- * @param time the creation time to use (instead of the current time)
- * @return The entry value
- */
- public static NetworkTableValue makeStringArray(String[] value, long time) {
- return new NetworkTableValue(NetworkTableType.kStringArray, value, time);
- }
-
- @Override
- public boolean equals(Object other) {
- if (other == this) {
- return true;
- }
- if (!(other instanceof NetworkTableValue)) {
- return false;
- }
- NetworkTableValue ntOther = (NetworkTableValue) other;
- return m_type == ntOther.m_type && m_value.equals(ntOther.m_value);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(m_type, m_value);
- }
-
- // arraycopy() doesn't know how to unwrap boxed values; this is a false positive in PMD
- // (see https://sourceforge.net/p/pmd/bugs/804/)
- @SuppressWarnings("PMD.AvoidArrayLoops")
- static boolean[] toNative(Boolean[] arr) {
- boolean[] out = new boolean[arr.length];
- for (int i = 0; i < arr.length; i++) {
- out[i] = arr[i];
- }
- return out;
- }
-
- @SuppressWarnings("PMD.AvoidArrayLoops")
- static double[] toNative(Number[] arr) {
- double[] out = new double[arr.length];
- for (int i = 0; i < arr.length; i++) {
- out[i] = arr[i].doubleValue();
- }
- return out;
- }
-
- @SuppressWarnings("PMD.AvoidArrayLoops")
- static Boolean[] fromNative(boolean[] arr) {
- Boolean[] out = new Boolean[arr.length];
- for (int i = 0; i < arr.length; i++) {
- out[i] = arr[i];
- }
- return out;
- }
-
- @SuppressWarnings("PMD.AvoidArrayLoops")
- static Double[] fromNative(double[] arr) {
- Double[] out = new Double[arr.length];
- for (int i = 0; i < arr.length; i++) {
- out[i] = arr[i];
- }
- return out;
- }
-
- private NetworkTableType m_type;
- private Object m_value;
- private long m_time;
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java
deleted file mode 100644
index 8c4916f..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java
+++ /dev/null
@@ -1,275 +0,0 @@
-// 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.RuntimeLoader;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public final class NetworkTablesJNI {
- static boolean libraryLoaded = false;
- static RuntimeLoader<NetworkTablesJNI> loader = null;
-
- public static class Helper {
- private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
-
- public static boolean getExtractOnStaticLoad() {
- return extractOnStaticLoad.get();
- }
-
- public static void setExtractOnStaticLoad(boolean load) {
- extractOnStaticLoad.set(load);
- }
- }
-
- static {
- if (Helper.getExtractOnStaticLoad()) {
- try {
- loader =
- new RuntimeLoader<>(
- "ntcorejni", RuntimeLoader.getDefaultExtractionRoot(), NetworkTablesJNI.class);
- loader.loadLibrary();
- } catch (IOException ex) {
- ex.printStackTrace();
- System.exit(1);
- }
- libraryLoaded = true;
- }
- }
-
- /**
- * Force load the library.
- *
- * @throws IOException if the library fails to load
- */
- public static synchronized void forceLoad() throws IOException {
- if (libraryLoaded) {
- return;
- }
- loader =
- new RuntimeLoader<>(
- "ntcorejni", RuntimeLoader.getDefaultExtractionRoot(), NetworkTablesJNI.class);
- loader.loadLibrary();
- libraryLoaded = true;
- }
-
- public static native int getDefaultInstance();
-
- public static native int createInstance();
-
- public static native void destroyInstance(int inst);
-
- public static native int getInstanceFromHandle(int handle);
-
- public static native int getEntry(int inst, String key);
-
- public static native int[] getEntries(int inst, String prefix, int types);
-
- public static native String getEntryName(int entry);
-
- public static native long getEntryLastChange(int entry);
-
- public static native int getType(int entry);
-
- public static native boolean setBoolean(int entry, long time, boolean value, boolean force);
-
- public static native boolean setDouble(int entry, long time, double value, boolean force);
-
- public static native boolean setString(int entry, long time, String value, boolean force);
-
- public static native boolean setRaw(int entry, long time, byte[] value, boolean force);
-
- public static native boolean setRaw(
- int entry, long time, ByteBuffer value, int len, boolean force);
-
- public static native boolean setBooleanArray(
- int entry, long time, boolean[] value, boolean force);
-
- public static native boolean setDoubleArray(int entry, long time, double[] value, boolean force);
-
- public static native boolean setStringArray(int entry, long time, String[] value, boolean force);
-
- public static native NetworkTableValue getValue(int entry);
-
- public static native boolean getBoolean(int entry, boolean defaultValue);
-
- public static native double getDouble(int entry, double defaultValue);
-
- public static native String getString(int entry, String defaultValue);
-
- public static native byte[] getRaw(int entry, byte[] defaultValue);
-
- public static native boolean[] getBooleanArray(int entry, boolean[] defaultValue);
-
- public static native double[] getDoubleArray(int entry, double[] defaultValue);
-
- public static native String[] getStringArray(int entry, String[] defaultValue);
-
- public static native boolean setDefaultBoolean(int entry, long time, boolean defaultValue);
-
- public static native boolean setDefaultDouble(int entry, long time, double defaultValue);
-
- public static native boolean setDefaultString(int entry, long time, String defaultValue);
-
- public static native boolean setDefaultRaw(int entry, long time, byte[] defaultValue);
-
- public static native boolean setDefaultBooleanArray(int entry, long time, boolean[] defaultValue);
-
- public static native boolean setDefaultDoubleArray(int entry, long time, double[] defaultValue);
-
- public static native boolean setDefaultStringArray(int entry, long time, String[] defaultValue);
-
- public static native void setEntryFlags(int entry, int flags);
-
- public static native int getEntryFlags(int entry);
-
- public static native void deleteEntry(int entry);
-
- public static native void deleteAllEntries(int inst);
-
- public static native EntryInfo getEntryInfoHandle(NetworkTableInstance inst, int entry);
-
- public static native EntryInfo[] getEntryInfo(
- NetworkTableInstance instObject, int inst, String prefix, int types);
-
- public static native int createEntryListenerPoller(int inst);
-
- public static native void destroyEntryListenerPoller(int poller);
-
- public static native int addPolledEntryListener(int poller, String prefix, int flags);
-
- public static native int addPolledEntryListener(int poller, int entry, int flags);
-
- public static native EntryNotification[] pollEntryListener(NetworkTableInstance inst, int poller)
- throws InterruptedException;
-
- public static native EntryNotification[] pollEntryListenerTimeout(
- NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
-
- public static native void cancelPollEntryListener(int poller);
-
- public static native void removeEntryListener(int entryListener);
-
- public static native boolean waitForEntryListenerQueue(int inst, double timeout);
-
- public static native int createConnectionListenerPoller(int inst);
-
- public static native void destroyConnectionListenerPoller(int poller);
-
- public static native int addPolledConnectionListener(int poller, boolean immediateNotify);
-
- public static native ConnectionNotification[] pollConnectionListener(
- NetworkTableInstance inst, int poller) throws InterruptedException;
-
- public static native ConnectionNotification[] pollConnectionListenerTimeout(
- NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
-
- public static native void cancelPollConnectionListener(int poller);
-
- public static native void removeConnectionListener(int connListener);
-
- public static native boolean waitForConnectionListenerQueue(int inst, double timeout);
-
- public static native int createRpcCallPoller(int inst);
-
- public static native void destroyRpcCallPoller(int poller);
-
- public static native void createPolledRpc(int entry, byte[] def, int poller);
-
- public static native RpcAnswer[] pollRpc(NetworkTableInstance inst, int poller)
- throws InterruptedException;
-
- public static native RpcAnswer[] pollRpcTimeout(
- NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
-
- public static native void cancelPollRpc(int poller);
-
- public static native boolean waitForRpcCallQueue(int inst, double timeout);
-
- public static native boolean postRpcResponse(int entry, int call, byte[] result);
-
- public static native int callRpc(int entry, byte[] params);
-
- public static native byte[] getRpcResult(int entry, int call);
-
- public static native byte[] getRpcResult(int entry, int call, double timeout);
-
- public static native void cancelRpcResult(int entry, int call);
-
- public static native byte[] getRpc(int entry, byte[] defaultValue);
-
- public static native void setNetworkIdentity(int inst, String name);
-
- public static native int getNetworkMode(int inst);
-
- public static native void startLocal(int inst);
-
- public static native void stopLocal(int inst);
-
- public static native void startServer(
- int inst, String persistFilename, String listenAddress, int port);
-
- public static native void stopServer(int inst);
-
- public static native void startClient(int inst);
-
- public static native void startClient(int inst, String serverName, int port);
-
- public static native void startClient(int inst, String[] serverNames, int[] ports);
-
- public static native void startClientTeam(int inst, int team, int port);
-
- public static native void stopClient(int inst);
-
- public static native void setServer(int inst, String serverName, int port);
-
- public static native void setServer(int inst, String[] serverNames, int[] ports);
-
- public static native void setServerTeam(int inst, int team, int port);
-
- public static native void startDSClient(int inst, int port);
-
- public static native void stopDSClient(int inst);
-
- public static native void setUpdateRate(int inst, double interval);
-
- public static native void flush(int inst);
-
- public static native ConnectionInfo[] getConnections(int inst);
-
- public static native boolean isConnected(int inst);
-
- public static native void savePersistent(int inst, String filename) throws PersistentException;
-
- public static native String[] loadPersistent(int inst, String filename)
- throws PersistentException; // returns warnings
-
- public static native void saveEntries(int inst, String filename, String prefix)
- throws PersistentException;
-
- public static native String[] loadEntries(int inst, String filename, String prefix)
- throws PersistentException; // returns warnings
-
- public static native long now();
-
- public static native int createLoggerPoller(int inst);
-
- public static native void destroyLoggerPoller(int poller);
-
- public static native int addPolledLogger(int poller, int minLevel, int maxLevel);
-
- public static native LogMessage[] pollLogger(NetworkTableInstance inst, int poller)
- throws InterruptedException;
-
- public static native LogMessage[] pollLoggerTimeout(
- NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
-
- public static native void cancelPollLogger(int poller);
-
- public static native void removeLogger(int logger);
-
- public static native boolean waitForLoggerQueue(int inst, double timeout);
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/PersistentException.java b/ntcore/src/main/java/edu/wpi/first/networktables/PersistentException.java
deleted file mode 100644
index 1c61595..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/PersistentException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// 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.io.IOException;
-
-/** An exception thrown when persistent load/save fails in a {@link NetworkTable}. */
-public final class PersistentException extends IOException {
- public static final long serialVersionUID = 0;
-
- public PersistentException(String message) {
- super(message);
- }
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/PubSub.java b/ntcore/src/main/java/edu/wpi/first/networktables/PubSub.java
new file mode 100644
index 0000000..7beaf9e
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/PubSub.java
@@ -0,0 +1,32 @@
+// 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 publisher or subscriber. */
+public interface PubSub extends AutoCloseable {
+ @Override
+ void close();
+
+ /**
+ * Gets the subscribed-to / published-to topic.
+ *
+ * @return Topic
+ */
+ Topic getTopic();
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ boolean isValid();
+
+ /**
+ * Gets the native handle.
+ *
+ * @return Handle
+ */
+ int getHandle();
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/PubSubOption.java b/ntcore/src/main/java/edu/wpi/first/networktables/PubSubOption.java
new file mode 100644
index 0000000..dd96eec
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/PubSubOption.java
@@ -0,0 +1,156 @@
+// 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 publish/subscribe option. */
+public class PubSubOption {
+ enum Kind {
+ periodic,
+ sendAll,
+ topicsOnly,
+ pollStorage,
+ keepDuplicates,
+ disableRemote,
+ disableLocal,
+ excludePublisher,
+ excludeSelf
+ }
+
+ PubSubOption(Kind kind, boolean value) {
+ m_kind = kind;
+ m_bValue = value;
+ m_iValue = 0;
+ m_dValue = 0;
+ }
+
+ PubSubOption(Kind kind, int value) {
+ m_kind = kind;
+ m_bValue = false;
+ m_iValue = value;
+ m_dValue = 0;
+ }
+
+ PubSubOption(Kind kind, double value) {
+ m_kind = kind;
+ m_bValue = false;
+ m_iValue = 0;
+ m_dValue = value;
+ }
+
+ /**
+ * How frequently changes will be sent over the network. NetworkTables may send more frequently
+ * than this (e.g. use a combined minimum period for all values) or apply a restricted range to
+ * this value. The default if unspecified is 100 ms.
+ *
+ * @param period time between updates, in seconds
+ * @return option
+ */
+ public static PubSubOption periodic(double period) {
+ return new PubSubOption(Kind.periodic, period);
+ }
+
+ /**
+ * If enabled, sends all value changes over the network. This option defaults to disabled.
+ *
+ * @param enabled True to enable, false to disable
+ * @return option
+ */
+ public static PubSubOption sendAll(boolean enabled) {
+ return new PubSubOption(Kind.sendAll, enabled);
+ }
+
+ /**
+ * If enabled on a subscription, does not request value changes. This option defaults to disabled.
+ *
+ * @param enabled True to enable, false to disable
+ * @return option
+ */
+ public static PubSubOption topicsOnly(boolean enabled) {
+ return new PubSubOption(Kind.topicsOnly, enabled);
+ }
+
+ /**
+ * If enabled, preserves duplicate value changes (rather than ignoring them). This option defaults
+ * to disabled.
+ *
+ * @param enabled True to enable, false to disable
+ * @return option
+ */
+ public static PubSubOption keepDuplicates(boolean enabled) {
+ return new PubSubOption(Kind.keepDuplicates, enabled);
+ }
+
+ /**
+ * Polling storage for subscription. Specifies the maximum number of updates NetworkTables should
+ * store between calls to the subscriber's readQueue() function. Defaults to 1 if sendAll is
+ * false, 20 if sendAll is true.
+ *
+ * @param depth number of entries to save for polling.
+ * @return option
+ */
+ public static PubSubOption pollStorage(int depth) {
+ return new PubSubOption(Kind.pollStorage, depth);
+ }
+
+ /**
+ * For subscriptions, specify whether remote value updates should not be queued for readQueue().
+ * See also disableLocal(). Defaults to false (remote value updates are queued).
+ *
+ * @param disabled True to disable, false to enable
+ * @return option
+ */
+ public static PubSubOption disableRemote(boolean disabled) {
+ return new PubSubOption(Kind.disableRemote, disabled);
+ }
+
+ /**
+ * For subscriptions, specify whether local value updates should not be queued for readQueue().
+ * See also disableRemote(). Defaults to false (local value updates are queued).
+ *
+ * @param disabled True to disable, false to enable
+ * @return option
+ */
+ public static PubSubOption disableLocal(boolean disabled) {
+ return new PubSubOption(Kind.disableLocal, disabled);
+ }
+
+ /**
+ * Don't queue value updates for the given publisher. Only has an effect on subscriptions. Only
+ * one exclusion may be set.
+ *
+ * @param publisher publisher handle to exclude
+ * @return option
+ */
+ public static PubSubOption excludePublisher(int publisher) {
+ return new PubSubOption(Kind.excludePublisher, publisher);
+ }
+
+ /**
+ * Don't queue value updates for the given publisher. Only has an effect on subscriptions. Only
+ * one exclusion may be set.
+ *
+ * @param publisher publisher to exclude
+ * @return option
+ */
+ public static PubSubOption excludePublisher(Publisher publisher) {
+ return excludePublisher(publisher != null ? publisher.getHandle() : 0);
+ }
+
+ /**
+ * Don't queue value updates for the internal publisher for an entry. Only has an effect on
+ * entries.
+ *
+ * @param enabled True to enable, false to disable
+ * @return option
+ */
+ public static PubSubOption excludeSelf(boolean enabled) {
+ return new PubSubOption(Kind.excludeSelf, enabled);
+ }
+
+ final Kind m_kind;
+ final boolean m_bValue;
+ final int m_iValue;
+ final double m_dValue;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/PubSubOptions.java b/ntcore/src/main/java/edu/wpi/first/networktables/PubSubOptions.java
new file mode 100644
index 0000000..18557b9
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/PubSubOptions.java
@@ -0,0 +1,126 @@
+// 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 publish/subscribe options. */
+@SuppressWarnings("MemberName")
+public class PubSubOptions {
+ /**
+ * Construct from a list of options.
+ *
+ * @param options options
+ */
+ public PubSubOptions(PubSubOption... options) {
+ for (PubSubOption option : options) {
+ switch (option.m_kind) {
+ case periodic:
+ periodic = option.m_dValue;
+ break;
+ case sendAll:
+ sendAll = option.m_bValue;
+ break;
+ case topicsOnly:
+ topicsOnly = option.m_bValue;
+ break;
+ case pollStorage:
+ pollStorage = option.m_iValue;
+ break;
+ case keepDuplicates:
+ keepDuplicates = option.m_bValue;
+ break;
+ case disableRemote:
+ disableRemote = option.m_bValue;
+ break;
+ case disableLocal:
+ disableLocal = option.m_bValue;
+ break;
+ case excludePublisher:
+ excludePublisher = option.m_iValue;
+ break;
+ case excludeSelf:
+ excludeSelf = option.m_bValue;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ PubSubOptions(
+ int pollStorage,
+ double periodic,
+ int excludePublisher,
+ boolean sendAll,
+ boolean topicsOnly,
+ boolean keepDuplicates,
+ boolean prefixMatch,
+ boolean disableRemote,
+ boolean disableLocal,
+ boolean excludeSelf) {
+ this.pollStorage = pollStorage;
+ this.periodic = periodic;
+ this.excludePublisher = excludePublisher;
+ this.sendAll = sendAll;
+ this.topicsOnly = topicsOnly;
+ this.keepDuplicates = keepDuplicates;
+ this.prefixMatch = prefixMatch;
+ this.disableRemote = disableRemote;
+ this.disableLocal = disableLocal;
+ this.excludeSelf = excludeSelf;
+ }
+
+ /** Default value of periodic. */
+ public static final double kDefaultPeriodic = 0.1;
+
+ /**
+ * Polling storage size for a subscription. Specifies the maximum number of updates NetworkTables
+ * should store between calls to the subscriber's readQueue() function. If zero, defaults to 1 if
+ * sendAll is false, 20 if sendAll is true.
+ */
+ public int pollStorage;
+
+ /**
+ * How frequently changes will be sent over the network, in seconds. NetworkTables may send more
+ * frequently than this (e.g. use a combined minimum period for all values) or apply a restricted
+ * range to this value. The default is 100 ms.
+ */
+ public double periodic = kDefaultPeriodic;
+
+ /**
+ * For subscriptions, if non-zero, value updates for readQueue() are not queued for this
+ * publisher.
+ */
+ public int excludePublisher;
+
+ /** Send all value changes over the network. */
+ public boolean sendAll;
+
+ /** For subscriptions, don't ask for value changes (only topic announcements). */
+ public boolean topicsOnly;
+
+ /** Preserve duplicate value changes (rather than ignoring them). */
+ public boolean keepDuplicates;
+
+ /**
+ * Perform prefix match on subscriber topic names. Is ignored/overridden by subscribe() functions;
+ * only present in struct for the purposes of getting information about subscriptions.
+ */
+ public boolean prefixMatch;
+
+ /**
+ * For subscriptions, if remote value updates should not be queued for readQueue(). See also
+ * disableLocal.
+ */
+ public boolean disableRemote;
+
+ /**
+ * For subscriptions, if local value updates should not be queued for readQueue(). See also
+ * disableRemote.
+ */
+ public boolean disableLocal;
+
+ /** For entries, don't queue (for readQueue) value updates for the entry's internal publisher. */
+ public boolean excludeSelf;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/Publisher.java b/ntcore/src/main/java/edu/wpi/first/networktables/Publisher.java
new file mode 100644
index 0000000..616c7ab
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/Publisher.java
@@ -0,0 +1,8 @@
+// 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 publisher. */
+public interface Publisher extends PubSub {}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java b/ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java
deleted file mode 100644
index a244bde..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// 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 Remote Procedure Call (Server Side). */
-public final class RpcAnswer {
- /** Entry handle. */
- @SuppressWarnings("MemberName")
- public final int entry;
-
- /** Call handle. */
- @SuppressWarnings("MemberName")
- public int call;
-
- /** Entry name. */
- @SuppressWarnings("MemberName")
- public final String name;
-
- /** Call raw parameters. */
- @SuppressWarnings("MemberName")
- public final byte[] params;
-
- /** Connection that called the RPC. */
- @SuppressWarnings("MemberName")
- public final ConnectionInfo conn;
-
- /**
- * Constructor. This should generally only be used internally to NetworkTables.
- *
- * @param inst Instance
- * @param entry Entry handle
- * @param call Call handle
- * @param name Entry name
- * @param params Call raw parameters
- * @param conn Connection info
- */
- @SuppressWarnings("PMD.ArrayIsStoredDirectly")
- public RpcAnswer(
- NetworkTableInstance inst,
- int entry,
- int call,
- String name,
- byte[] params,
- ConnectionInfo conn) {
- this.m_inst = inst;
- this.entry = entry;
- this.call = call;
- this.name = name;
- this.params = params;
- this.conn = conn;
- }
-
- static final byte[] emptyResponse = new byte[] {};
-
- /*
- * Finishes an RPC answer by replying empty if the user did not respond.
- * Called internally by the callback thread.
- */
- void finish() {
- if (call != 0) {
- NetworkTablesJNI.postRpcResponse(entry, call, emptyResponse);
- call = 0;
- }
- }
-
- /**
- * Determines if the native handle is valid.
- *
- * @return True if the native handle is valid, false otherwise.
- */
- public boolean isValid() {
- return call != 0;
- }
-
- /**
- * Post RPC response (return value) for a polled RPC.
- *
- * @param result result raw data that will be provided to remote caller
- * @return true if the response was posted, otherwise false
- */
- public boolean postResponse(byte[] result) {
- boolean ret = NetworkTablesJNI.postRpcResponse(entry, call, result);
- call = 0;
- return ret;
- }
-
- /* Network table instance. */
- private final NetworkTableInstance m_inst;
-
- /* Cached entry object. */
- NetworkTableEntry m_entryObject;
-
- /**
- * Get the entry as an object.
- *
- * @return NetworkTableEntry for the RPC.
- */
- NetworkTableEntry getEntry() {
- if (m_entryObject == null) {
- m_entryObject = new NetworkTableEntry(m_inst, entry);
- }
- return m_entryObject;
- }
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/RpcCall.java b/ntcore/src/main/java/edu/wpi/first/networktables/RpcCall.java
deleted file mode 100644
index 790ff5f..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/RpcCall.java
+++ /dev/null
@@ -1,90 +0,0 @@
-// 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 Remote Procedure Call. */
-public final class RpcCall implements AutoCloseable {
- /**
- * Constructor. This should generally only be used internally to NetworkTables.
- *
- * @param entry Entry
- * @param call Call handle
- */
- public RpcCall(NetworkTableEntry entry, int call) {
- m_entry = entry;
- m_call = call;
- }
-
- /** Cancels the result if no other action taken. */
- @Override
- public synchronized void close() {
- if (m_call != 0) {
- cancelResult();
- }
- }
-
- /**
- * Determines if the native handle is valid.
- *
- * @return True if the native handle is valid, false otherwise.
- */
- public boolean isValid() {
- return m_call != 0;
- }
-
- /**
- * Get the RPC entry.
- *
- * @return NetworkTableEntry for the RPC.
- */
- public NetworkTableEntry getEntry() {
- return m_entry;
- }
-
- /**
- * Get the call native handle.
- *
- * @return Native handle.
- */
- public int getCall() {
- return m_call;
- }
-
- /**
- * Get the result (return value). This function blocks until the result is received.
- *
- * @return Received result (output)
- */
- public byte[] getResult() {
- byte[] result = NetworkTablesJNI.getRpcResult(m_entry.getHandle(), m_call);
- if (result.length != 0) {
- m_call = 0;
- }
- return result;
- }
-
- /**
- * Get the result (return value). This function blocks until the result is received or it times
- * out.
- *
- * @param timeout timeout, in seconds
- * @return Received result (output)
- */
- public byte[] getResult(double timeout) {
- byte[] result = NetworkTablesJNI.getRpcResult(m_entry.getHandle(), m_call, timeout);
- if (result.length != 0) {
- m_call = 0;
- }
- return result;
- }
-
- /** Ignore the result. This function is non-blocking. */
- public void cancelResult() {
- NetworkTablesJNI.cancelRpcResult(m_entry.getHandle(), m_call);
- }
-
- private final NetworkTableEntry m_entry;
- private int m_call;
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/Subscriber.java b/ntcore/src/main/java/edu/wpi/first/networktables/Subscriber.java
new file mode 100644
index 0000000..08f825d
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/Subscriber.java
@@ -0,0 +1,22 @@
+// 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 subscriber. */
+public interface Subscriber extends PubSub {
+ /**
+ * Determines if the entry currently exists.
+ *
+ * @return True if the entry exists, false otherwise.
+ */
+ boolean exists();
+
+ /**
+ * Gets the last time the entry's value was changed.
+ *
+ * @return Entry last change time
+ */
+ long getLastChange();
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TableEntryListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/TableEntryListener.java
deleted file mode 100644
index e0781e1..0000000
--- a/ntcore/src/main/java/edu/wpi/first/networktables/TableEntryListener.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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;
-
-/** A listener that listens to changes in values in a {@link NetworkTable}. */
-@FunctionalInterface
-public interface TableEntryListener extends EntryListenerFlags {
- /**
- * Called when a key-value pair is changed in a {@link NetworkTable}.
- *
- * @param table the table the key-value pair exists in
- * @param key the key associated with the value that changed
- * @param entry the entry associated with the value that changed
- * @param value the new value
- * @param flags update flags; for example, EntryListenerFlags.kNew if the key did not previously
- * exist in the table
- */
- void valueChanged(
- NetworkTable table, String key, NetworkTableEntry entry, NetworkTableValue value, int flags);
-}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TimeSyncEventData.java b/ntcore/src/main/java/edu/wpi/first/networktables/TimeSyncEventData.java
new file mode 100644
index 0000000..f8710ad
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/TimeSyncEventData.java
@@ -0,0 +1,37 @@
+// 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 time sync event data. */
+@SuppressWarnings("MemberName")
+public final class TimeSyncEventData {
+ /**
+ * Offset between local time and server time, in microseconds. Add this value to local time to get
+ * the estimated equivalent server time.
+ */
+ public final long serverTimeOffset;
+
+ /** Measured round trip time divided by 2, in microseconds. */
+ public final long rtt2;
+
+ /**
+ * If serverTimeOffset and RTT are valid. An event with this set to false is sent when the client
+ * disconnects.
+ */
+ public final boolean valid;
+
+ /**
+ * Constructor. This should generally only be used internally to NetworkTables.
+ *
+ * @param serverTimeOffset Server time offset
+ * @param rtt2 Round trip time divided by 2
+ * @param valid If other parameters are valid
+ */
+ public TimeSyncEventData(long serverTimeOffset, long rtt2, boolean valid) {
+ this.serverTimeOffset = serverTimeOffset;
+ this.rtt2 = rtt2;
+ this.valid = valid;
+ }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java b/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java
new file mode 100644
index 0000000..db08a34
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java
@@ -0,0 +1,322 @@
+// 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 Topic. */
+public class Topic {
+ /**
+ * Constructor; use NetworkTableInstance.getTopic() instead.
+ *
+ * @param inst Instance
+ * @param handle Native handle
+ */
+ Topic(NetworkTableInstance inst, int handle) {
+ m_inst = inst;
+ m_handle = handle;
+ }
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ public boolean isValid() {
+ return m_handle != 0;
+ }
+
+ /**
+ * Gets the native handle for the topic.
+ *
+ * @return Native handle
+ */
+ public int getHandle() {
+ return m_handle;
+ }
+
+ /**
+ * Gets the instance for the topic.
+ *
+ * @return Instance
+ */
+ public NetworkTableInstance getInstance() {
+ return m_inst;
+ }
+
+ /**
+ * Gets the name of the topic.
+ *
+ * @return the topic's name
+ */
+ public String getName() {
+ return NetworkTablesJNI.getTopicName(m_handle);
+ }
+
+ /**
+ * Gets the type of the topic.
+ *
+ * @return the topic's type
+ */
+ public NetworkTableType getType() {
+ return NetworkTableType.getFromInt(NetworkTablesJNI.getType(m_handle));
+ }
+
+ /**
+ * Gets the type string of the topic. This may have more information than the numeric type
+ * (especially for raw values).
+ *
+ * @return the topic's type
+ */
+ public String getTypeString() {
+ return NetworkTablesJNI.getTopicTypeString(m_handle);
+ }
+
+ /**
+ * Gets combined information about the topic.
+ *
+ * @return Topic information
+ */
+ public TopicInfo getInfo() {
+ return NetworkTablesJNI.getTopicInfo(m_inst, m_handle);
+ }
+
+ /**
+ * Make value persistent through server restarts.
+ *
+ * @param persistent True for persistent, false for not persistent.
+ */
+ public void setPersistent(boolean persistent) {
+ NetworkTablesJNI.setTopicPersistent(m_handle, persistent);
+ }
+
+ /**
+ * Returns whether the value is persistent through server restarts.
+ *
+ * @return True if the value is persistent.
+ */
+ public boolean isPersistent() {
+ return NetworkTablesJNI.getTopicPersistent(m_handle);
+ }
+
+ /**
+ * Make the server retain the topic even when there are no publishers.
+ *
+ * @param retained True for retained, false for not retained.
+ */
+ public void setRetained(boolean retained) {
+ NetworkTablesJNI.setTopicRetained(m_handle, retained);
+ }
+
+ /**
+ * Returns whether the topic is retained by server when there are no publishers.
+ *
+ * @return True if the topic is retained.
+ */
+ public boolean isRetained() {
+ return NetworkTablesJNI.getTopicRetained(m_handle);
+ }
+
+ /**
+ * Determines if the topic is currently being published.
+ *
+ * @return True if the topic exists, false otherwise.
+ */
+ public boolean exists() {
+ return NetworkTablesJNI.getTopicExists(m_handle);
+ }
+
+ /**
+ * Gets the current value of a property (as a JSON string).
+ *
+ * @param name property name
+ * @return JSON string; "null" if the property does not exist.
+ */
+ public String getProperty(String name) {
+ return NetworkTablesJNI.getTopicProperty(m_handle, name);
+ }
+
+ /**
+ * Sets a property value.
+ *
+ * @param name property name
+ * @param value property value (JSON string)
+ * @throws IllegalArgumentException if properties is not parseable as JSON
+ */
+ public void setProperty(String name, String value) {
+ NetworkTablesJNI.setTopicProperty(m_handle, name, value);
+ }
+
+ /**
+ * Deletes a property. Has no effect if the property does not exist.
+ *
+ * @param name property name
+ */
+ public void deleteProperty(String name) {
+ NetworkTablesJNI.deleteTopicProperty(m_handle, name);
+ }
+
+ /**
+ * Gets all topic properties as a JSON object string. Each key in the object is the property name,
+ * and the corresponding value is the property value.
+ *
+ * @return JSON string
+ */
+ public String getProperties() {
+ return NetworkTablesJNI.getTopicProperties(m_handle);
+ }
+
+ /**
+ * Updates multiple topic properties. Each key in the passed-in object is the name of the property
+ * to add/update, and the corresponding value is the property value to set for that property. Null
+ * values result in deletion of the corresponding property.
+ *
+ * @param properties JSON object string with keys to add/update/delete
+ * @throws IllegalArgumentException if properties is not a JSON object
+ */
+ public void setProperties(String properties) {
+ NetworkTablesJNI.setTopicProperties(m_handle, properties);
+ }
+
+ /**
+ * Create a new subscriber to the topic.
+ *
+ * <p>The subscriber is only active as long as the returned object is not closed.
+ *
+ * @param options subscribe options
+ * @return subscriber
+ */
+ public GenericSubscriber genericSubscribe(PubSubOption... options) {
+ return genericSubscribe("", options);
+ }
+
+ /**
+ * 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 typeString type string
+ * @param options subscribe options
+ * @return subscriber
+ */
+ public GenericSubscriber genericSubscribe(String typeString, PubSubOption... options) {
+ return new GenericEntryImpl(
+ this,
+ NetworkTablesJNI.subscribe(
+ m_handle, NetworkTableType.getFromString(typeString).getValue(), typeString, options));
+ }
+
+ /**
+ * 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 typeString type string
+ * @param options publish options
+ * @return publisher
+ */
+ public GenericPublisher genericPublish(String typeString, PubSubOption... options) {
+ return new GenericEntryImpl(
+ this,
+ NetworkTablesJNI.publish(
+ m_handle, NetworkTableType.getFromString(typeString).getValue(), typeString, options));
+ }
+
+ /**
+ * 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 typeString type string
+ * @param properties JSON properties
+ * @param options publish options
+ * @return publisher
+ * @throws IllegalArgumentException if properties is not a JSON object
+ */
+ public GenericPublisher genericPublishEx(
+ String typeString, String properties, PubSubOption... options) {
+ return new GenericEntryImpl(
+ this,
+ NetworkTablesJNI.publishEx(
+ m_handle,
+ NetworkTableType.getFromString(typeString).getValue(),
+ typeString,
+ properties,
+ options));
+ }
+
+ /**
+ * Create a new generic 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 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 and/or subscribe options
+ * @return entry
+ */
+ public GenericEntry getGenericEntry(PubSubOption... options) {
+ return getGenericEntry("", options);
+ }
+
+ /**
+ * Create a new generic 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 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 typeString type string
+ * @param options publish and/or subscribe options
+ * @return entry
+ */
+ public GenericEntry getGenericEntry(String typeString, PubSubOption... options) {
+ return new GenericEntryImpl(
+ this,
+ NetworkTablesJNI.getEntry(
+ m_handle, NetworkTableType.getFromString(typeString).getValue(), typeString, options));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof Topic)) {
+ return false;
+ }
+
+ return m_handle == ((Topic) other).m_handle;
+ }
+
+ @Override
+ public int hashCode() {
+ return m_handle;
+ }
+
+ protected NetworkTableInstance m_inst;
+ protected int m_handle;
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TopicInfo.java b/ntcore/src/main/java/edu/wpi/first/networktables/TopicInfo.java
new file mode 100644
index 0000000..abd8c2a
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/TopicInfo.java
@@ -0,0 +1,65 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables topic information. */
+@SuppressWarnings("MemberName")
+public final class TopicInfo {
+ /** Topic handle. */
+ public final int topic;
+
+ /** Topic name. */
+ public final String name;
+
+ /** Topic type. */
+ public final NetworkTableType type;
+
+ /** Topic type string. */
+ public final String typeStr;
+
+ /**
+ * Constructor. This should generally only be used internally to NetworkTables.
+ *
+ * @param inst Instance
+ * @param handle Topic handle
+ * @param name Name
+ * @param type Type (integer version of {@link NetworkTableType})
+ * @param typeStr Type string
+ */
+ public TopicInfo(NetworkTableInstance inst, int handle, String name, int type, String typeStr) {
+ this.m_inst = inst;
+ this.topic = handle;
+ this.name = name;
+ this.type = NetworkTableType.getFromInt(type);
+ this.typeStr = typeStr;
+ }
+
+ /* Network table instance. */
+ private final NetworkTableInstance m_inst;
+
+ /* Cached topic object. */
+ private Topic m_topicObject;
+
+ /**
+ * Get the instance.
+ *
+ * @return Instance
+ */
+ public NetworkTableInstance getInstance() {
+ return m_inst;
+ }
+
+ /**
+ * Get the topic as an object.
+ *
+ * @return Topic
+ */
+ public Topic getTopic() {
+ if (m_topicObject == null) {
+ m_topicObject = new Topic(m_inst, topic);
+ }
+ return m_topicObject;
+ }
+}
diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ValueEventData.java b/ntcore/src/main/java/edu/wpi/first/networktables/ValueEventData.java
new file mode 100644
index 0000000..511bb66
--- /dev/null
+++ b/ntcore/src/main/java/edu/wpi/first/networktables/ValueEventData.java
@@ -0,0 +1,54 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.networktables;
+
+/** NetworkTables value event data. */
+@SuppressWarnings("MemberName")
+public final class ValueEventData {
+ /** Topic handle. Topic.getHandle() can be used to map this to the corresponding Topic object. */
+ public final int topic;
+
+ /**
+ * Subscriber/entry handle. Subscriber.getHandle() or entry.getHandle() can be used to map this to
+ * the corresponding Subscriber or Entry object.
+ */
+ public final int subentry;
+
+ /** The new value. */
+ public final NetworkTableValue value;
+
+ /**
+ * Constructor. This should generally only be used internally to NetworkTables.
+ *
+ * @param inst Instance
+ * @param topic Topic handle
+ * @param subentry Subscriber/entry handle
+ * @param value The new value
+ */
+ public ValueEventData(
+ NetworkTableInstance inst, int topic, int subentry, NetworkTableValue value) {
+ this.m_inst = inst;
+ this.topic = topic;
+ this.subentry = subentry;
+ this.value = value;
+ }
+
+ /* Cached topic object. */
+ private Topic m_topicObject;
+
+ private final NetworkTableInstance m_inst;
+
+ /**
+ * Get the topic as an object.
+ *
+ * @return Topic for this notification.
+ */
+ public Topic getTopic() {
+ if (m_topicObject == null) {
+ m_topicObject = new Topic(m_inst, topic);
+ }
+ return m_topicObject;
+ }
+}
diff --git a/ntcore/src/main/native/cpp/ConnectionList.cpp b/ntcore/src/main/native/cpp/ConnectionList.cpp
new file mode 100644
index 0000000..4781f52
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ConnectionList.cpp
@@ -0,0 +1,126 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "ConnectionList.h"
+
+#include <wpi/SmallVector.h>
+#include <wpi/json_serializer.h>
+#include <wpi/raw_ostream.h>
+
+#include "IListenerStorage.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+using namespace nt;
+
+static std::string ConnInfoToJson(bool connected, const ConnectionInfo& info) {
+ std::string str;
+ wpi::raw_string_ostream os{str};
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"connected\":" << (connected ? "true" : "false");
+ os << ",\"remote_id\":\"";
+ s.dump_escaped(info.remote_id, false);
+ os << "\",\"remote_ip\":\"";
+ s.dump_escaped(info.remote_ip, false);
+ os << "\",\"remote_port\":";
+ s.dump_integer(static_cast<uint64_t>(info.remote_port));
+ os << ",\"protocol_version\":";
+ s.dump_integer(static_cast<uint64_t>(info.protocol_version));
+ os << "}";
+ os.flush();
+ return str;
+}
+
+ConnectionList::ConnectionList(int inst, IListenerStorage& listenerStorage)
+ : m_inst{inst}, m_listenerStorage{listenerStorage} {}
+
+ConnectionList::~ConnectionList() = default;
+
+int ConnectionList::AddConnection(const ConnectionInfo& info) {
+ std::scoped_lock lock{m_mutex};
+ m_connected = true;
+ m_listenerStorage.Notify({}, NT_EVENT_CONNECTED, &info);
+ if (!m_dataloggers.empty()) {
+ auto now = Now();
+ for (auto&& datalogger : m_dataloggers) {
+ datalogger->entry.Append(ConnInfoToJson(true, info), now);
+ }
+ }
+ return m_connections.emplace_back(info);
+}
+
+void ConnectionList::RemoveConnection(int handle) {
+ std::scoped_lock lock{m_mutex};
+ auto val = m_connections.erase(handle);
+ if (m_connections.empty()) {
+ m_connected = false;
+ }
+ if (val) {
+ m_listenerStorage.Notify({}, NT_EVENT_DISCONNECTED, &(*val));
+ if (!m_dataloggers.empty()) {
+ auto now = Now();
+ for (auto&& datalogger : m_dataloggers) {
+ datalogger->entry.Append(ConnInfoToJson(false, *val), now);
+ }
+ }
+ }
+}
+
+void ConnectionList::ClearConnections() {
+ std::scoped_lock lock{m_mutex};
+ m_connected = false;
+ for (auto&& conn : m_connections) {
+ m_listenerStorage.Notify({}, NT_EVENT_DISCONNECTED, &(*conn));
+ }
+ m_connections.clear();
+}
+
+std::vector<ConnectionInfo> ConnectionList::GetConnections() const {
+ std::scoped_lock lock{m_mutex};
+ std::vector<ConnectionInfo> info;
+ info.reserve(m_connections.size());
+ for (auto&& conn : m_connections) {
+ info.emplace_back(*conn);
+ }
+ return info;
+}
+
+bool ConnectionList::IsConnected() const {
+ return m_connected;
+}
+
+void ConnectionList::AddListener(NT_Listener listener, unsigned int eventMask) {
+ std::scoped_lock lock{m_mutex};
+ eventMask &= (NT_EVENT_CONNECTION | NT_EVENT_IMMEDIATE);
+ m_listenerStorage.Activate(listener, eventMask);
+ if ((eventMask & (NT_EVENT_CONNECTED | NT_EVENT_IMMEDIATE)) ==
+ (NT_EVENT_CONNECTED | NT_EVENT_IMMEDIATE) &&
+ !m_connections.empty()) {
+ wpi::SmallVector<const ConnectionInfo*, 16> infos;
+ infos.reserve(m_connections.size());
+ for (auto&& conn : m_connections) {
+ infos.emplace_back(&(*conn));
+ }
+ m_listenerStorage.Notify({&listener, 1},
+ NT_EVENT_CONNECTED | NT_EVENT_IMMEDIATE, infos);
+ }
+}
+
+NT_ConnectionDataLogger ConnectionList::StartDataLog(wpi::log::DataLog& log,
+ std::string_view name) {
+ std::scoped_lock lock{m_mutex};
+ auto now = Now();
+ auto datalogger = m_dataloggers.Add(m_inst, log, name, now);
+ for (auto&& conn : m_connections) {
+ datalogger->entry.Append(ConnInfoToJson(true, *conn), now);
+ }
+ return datalogger->handle;
+}
+
+void ConnectionList::StopDataLog(NT_ConnectionDataLogger logger) {
+ std::scoped_lock lock{m_mutex};
+ if (auto datalogger = m_dataloggers.Remove(logger)) {
+ datalogger->entry.Finish(Now());
+ }
+}
diff --git a/ntcore/src/main/native/cpp/ConnectionList.h b/ntcore/src/main/native/cpp/ConnectionList.h
new file mode 100644
index 0000000..c46266d
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ConnectionList.h
@@ -0,0 +1,74 @@
+// 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 <functional>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+#include <wpi/DataLog.h>
+#include <wpi/UidVector.h>
+#include <wpi/mutex.h>
+
+#include "Handle.h"
+#include "HandleMap.h"
+#include "IConnectionList.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class IListenerStorage;
+
+class ConnectionList final : public IConnectionList {
+ public:
+ ConnectionList(int inst, IListenerStorage& listenerStorage);
+ ~ConnectionList() final;
+
+ // IConnectionList interface
+ int AddConnection(const ConnectionInfo& info) final;
+ void RemoveConnection(int handle) final;
+ void ClearConnections() final;
+
+ // user-facing functions
+ std::vector<ConnectionInfo> GetConnections() const final;
+ bool IsConnected() const final;
+
+ void AddListener(NT_Listener listener, unsigned int eventMask);
+
+ NT_ConnectionDataLogger StartDataLog(wpi::log::DataLog& log,
+ std::string_view name);
+ void StopDataLog(NT_ConnectionDataLogger logger);
+
+ private:
+ int m_inst;
+ IListenerStorage& m_listenerStorage;
+ mutable wpi::mutex m_mutex;
+
+ // shared with user (must be atomic or mutex-protected)
+ std::atomic_bool m_connected{false};
+ wpi::UidVector<std::optional<ConnectionInfo>, 8> m_connections;
+
+ struct DataLoggerData {
+ static constexpr auto kType = Handle::kConnectionDataLogger;
+
+ DataLoggerData(NT_ConnectionDataLogger handle, wpi::log::DataLog& log,
+ std::string_view name, int64_t time)
+ : handle{handle},
+ entry{log, name,
+ "{\"schema\":\"NTConnectionInfo\",\"source\":\"NT\"}", "json",
+ time} {}
+
+ NT_ConnectionDataLogger handle;
+ wpi::log::StringLogEntry entry;
+ };
+ HandleMap<DataLoggerData, 8> m_dataloggers;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/ConnectionNotifier.cpp b/ntcore/src/main/native/cpp/ConnectionNotifier.cpp
deleted file mode 100644
index 60e7303..0000000
--- a/ntcore/src/main/native/cpp/ConnectionNotifier.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "ConnectionNotifier.h"
-
-using namespace nt;
-
-ConnectionNotifier::ConnectionNotifier(int inst) : m_inst(inst) {}
-
-void ConnectionNotifier::Start() {
- DoStart(m_inst);
-}
-
-unsigned int ConnectionNotifier::Add(
- std::function<void(const ConnectionNotification& event)> callback) {
- return DoAdd(callback);
-}
-
-unsigned int ConnectionNotifier::AddPolled(unsigned int poller_uid) {
- return DoAdd(poller_uid);
-}
-
-void ConnectionNotifier::NotifyConnection(bool connected,
- const ConnectionInfo& conn_info,
- unsigned int only_listener) {
- Send(only_listener, 0, connected, conn_info);
-}
diff --git a/ntcore/src/main/native/cpp/ConnectionNotifier.h b/ntcore/src/main/native/cpp/ConnectionNotifier.h
deleted file mode 100644
index fab4733..0000000
--- a/ntcore/src/main/native/cpp/ConnectionNotifier.h
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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.
-
-#ifndef NTCORE_CONNECTIONNOTIFIER_H_
-#define NTCORE_CONNECTIONNOTIFIER_H_
-
-#include <utility>
-
-#include <wpi/CallbackManager.h>
-
-#include "Handle.h"
-#include "IConnectionNotifier.h"
-#include "ntcore_cpp.h"
-
-namespace nt {
-
-namespace impl {
-
-class ConnectionNotifierThread
- : public wpi::CallbackThread<ConnectionNotifierThread,
- ConnectionNotification> {
- public:
- ConnectionNotifierThread(std::function<void()> on_start,
- std::function<void()> on_exit, int inst)
- : CallbackThread(std::move(on_start), std::move(on_exit)), m_inst(inst) {}
-
- bool Matches(const ListenerData& /*listener*/,
- const ConnectionNotification& /*data*/) {
- return true;
- }
-
- void SetListener(ConnectionNotification* data, unsigned int listener_uid) {
- data->listener =
- Handle(m_inst, listener_uid, Handle::kConnectionListener).handle();
- }
-
- void DoCallback(
- std::function<void(const ConnectionNotification& event)> callback,
- const ConnectionNotification& data) {
- callback(data);
- }
-
- int m_inst;
-};
-
-} // namespace impl
-
-class ConnectionNotifier
- : public IConnectionNotifier,
- public wpi::CallbackManager<ConnectionNotifier,
- impl::ConnectionNotifierThread> {
- friend class ConnectionNotifierTest;
- friend class wpi::CallbackManager<ConnectionNotifier,
- impl::ConnectionNotifierThread>;
-
- public:
- explicit ConnectionNotifier(int inst);
-
- void Start();
-
- unsigned int Add(std::function<void(const ConnectionNotification& event)>
- callback) override;
- unsigned int AddPolled(unsigned int poller_uid) override;
-
- void NotifyConnection(bool connected, const ConnectionInfo& conn_info,
- unsigned int only_listener = UINT_MAX) override;
-
- private:
- int m_inst;
-};
-
-} // namespace nt
-
-#endif // NTCORE_CONNECTIONNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/Dispatcher.cpp b/ntcore/src/main/native/cpp/Dispatcher.cpp
deleted file mode 100644
index 84d9698..0000000
--- a/ntcore/src/main/native/cpp/Dispatcher.cpp
+++ /dev/null
@@ -1,711 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "Dispatcher.h"
-
-#include <algorithm>
-#include <iterator>
-
-#include <wpi/SmallVector.h>
-#include <wpi/StringExtras.h>
-#include <wpi/TCPAcceptor.h>
-#include <wpi/TCPConnector.h>
-#include <wpi/timestamp.h>
-
-#include "IConnectionNotifier.h"
-#include "IStorage.h"
-#include "Log.h"
-#include "NetworkConnection.h"
-
-using namespace nt;
-
-void Dispatcher::StartServer(std::string_view persist_filename,
- const char* listen_address, unsigned int port) {
- std::string listen_address_copy(wpi::trim(listen_address));
- DispatcherBase::StartServer(
- persist_filename,
- std::unique_ptr<wpi::NetworkAcceptor>(new wpi::TCPAcceptor(
- static_cast<int>(port), listen_address_copy.c_str(), m_logger)));
-}
-
-void Dispatcher::SetServer(const char* server_name, unsigned int port) {
- std::string server_name_copy(wpi::trim(server_name));
- SetConnector([=]() -> std::unique_ptr<wpi::NetworkStream> {
- return wpi::TCPConnector::connect(server_name_copy.c_str(),
- static_cast<int>(port), m_logger, 1);
- });
-}
-
-void Dispatcher::SetServer(
- wpi::span<const std::pair<std::string_view, unsigned int>> servers) {
- wpi::SmallVector<std::pair<std::string, int>, 16> servers_copy;
- for (const auto& server : servers) {
- servers_copy.emplace_back(std::string{wpi::trim(server.first)},
- static_cast<int>(server.second));
- }
-
- SetConnector([=]() -> std::unique_ptr<wpi::NetworkStream> {
- wpi::SmallVector<std::pair<const char*, int>, 16> servers_copy2;
- for (const auto& server : servers_copy) {
- servers_copy2.emplace_back(server.first.c_str(), server.second);
- }
- return wpi::TCPConnector::connect_parallel(servers_copy2, m_logger, 1);
- });
-}
-
-void Dispatcher::SetServerTeam(unsigned int team, unsigned int port) {
- std::pair<std::string_view, unsigned int> servers[5];
-
- // 10.te.am.2
- auto fixed = fmt::format("10.{}.{}.2", static_cast<int>(team / 100),
- static_cast<int>(team % 100));
- servers[0] = {fixed, port};
-
- // 172.22.11.2
- servers[1] = {"172.22.11.2", port};
-
- // roboRIO-<team>-FRC.local
- auto mdns = fmt::format("roboRIO-{}-FRC.local", team);
- servers[2] = {mdns, port};
-
- // roboRIO-<team>-FRC.lan
- auto mdns_lan = fmt::format("roboRIO-{}-FRC.lan", team);
- servers[3] = {mdns_lan, port};
-
- // roboRIO-<team>-FRC.frc-field.local
- auto field_local = fmt::format("roboRIO-{}-FRC.frc-field.local", team);
- servers[4] = {field_local, port};
-
- SetServer(servers);
-}
-
-void Dispatcher::SetServerOverride(const char* server_name, unsigned int port) {
- std::string server_name_copy(wpi::trim(server_name));
- SetConnectorOverride([=]() -> std::unique_ptr<wpi::NetworkStream> {
- return wpi::TCPConnector::connect(server_name_copy.c_str(),
- static_cast<int>(port), m_logger, 1);
- });
-}
-
-void Dispatcher::ClearServerOverride() {
- ClearConnectorOverride();
-}
-
-DispatcherBase::DispatcherBase(IStorage& storage, IConnectionNotifier& notifier,
- wpi::Logger& logger)
- : m_storage(storage), m_notifier(notifier), m_logger(logger) {
- m_active = false;
- m_update_rate = 100;
-}
-
-DispatcherBase::~DispatcherBase() {
- Stop();
-}
-
-unsigned int DispatcherBase::GetNetworkMode() const {
- return m_networkMode;
-}
-
-void DispatcherBase::StartLocal() {
- {
- std::scoped_lock lock(m_user_mutex);
- if (m_active) {
- return;
- }
- m_active = true;
- }
- m_networkMode = NT_NET_MODE_LOCAL;
- m_storage.SetDispatcher(this, false);
-}
-
-void DispatcherBase::StartServer(
- std::string_view persist_filename,
- std::unique_ptr<wpi::NetworkAcceptor> acceptor) {
- {
- std::scoped_lock lock(m_user_mutex);
- if (m_active) {
- return;
- }
- m_active = true;
- }
- m_networkMode = NT_NET_MODE_SERVER | NT_NET_MODE_STARTING;
- m_persist_filename = persist_filename;
- m_server_acceptor = std::move(acceptor);
-
- // Load persistent file. Ignore errors, but pass along warnings.
- if (!persist_filename.empty()) {
- bool first = true;
- m_storage.LoadPersistent(
- persist_filename, [&](size_t line, const char* msg) {
- if (first) {
- first = false;
- WARNING("When reading initial persistent values from '{}':",
- persist_filename);
- }
- WARNING("{}:{}: {}", persist_filename, line, msg);
- });
- }
-
- m_storage.SetDispatcher(this, true);
-
- m_dispatch_thread = std::thread(&Dispatcher::DispatchThreadMain, this);
- m_clientserver_thread = std::thread(&Dispatcher::ServerThreadMain, this);
-}
-
-void DispatcherBase::StartClient() {
- {
- std::scoped_lock lock(m_user_mutex);
- if (m_active) {
- return;
- }
- m_active = true;
- }
- m_networkMode = NT_NET_MODE_CLIENT | NT_NET_MODE_STARTING;
- m_storage.SetDispatcher(this, false);
-
- m_dispatch_thread = std::thread(&Dispatcher::DispatchThreadMain, this);
- m_clientserver_thread = std::thread(&Dispatcher::ClientThreadMain, this);
-}
-
-void DispatcherBase::Stop() {
- m_active = false;
-
- // wake up dispatch thread with a flush
- m_flush_cv.notify_one();
-
- // wake up client thread with a reconnect
- {
- std::scoped_lock lock(m_user_mutex);
- m_client_connector = nullptr;
- }
- ClientReconnect();
-
- // wake up server thread by shutting down the socket
- if (m_server_acceptor) {
- m_server_acceptor->shutdown();
- }
-
- // join threads, with timeout
- if (m_dispatch_thread.joinable()) {
- m_dispatch_thread.join();
- }
- if (m_clientserver_thread.joinable()) {
- m_clientserver_thread.join();
- }
-
- std::vector<std::shared_ptr<INetworkConnection>> conns;
- {
- std::scoped_lock lock(m_user_mutex);
- conns.swap(m_connections);
- }
-
- // close all connections
- conns.resize(0);
-}
-
-void DispatcherBase::SetUpdateRate(double interval) {
- // don't allow update rates faster than 5 ms or slower than 1 second
- if (interval < 0.005) {
- interval = 0.005;
- } else if (interval > 1.0) {
- interval = 1.0;
- }
- m_update_rate = static_cast<unsigned int>(interval * 1000);
-}
-
-void DispatcherBase::SetIdentity(std::string_view name) {
- std::scoped_lock lock(m_user_mutex);
- m_identity = name;
-}
-
-void DispatcherBase::Flush() {
- auto now = wpi::Now();
- {
- std::scoped_lock lock(m_flush_mutex);
- // don't allow flushes more often than every 5 ms
- if ((now - m_last_flush) < 5000) {
- return;
- }
- m_last_flush = now;
- m_do_flush = true;
- }
- m_flush_cv.notify_one();
-}
-
-std::vector<ConnectionInfo> DispatcherBase::GetConnections() const {
- std::vector<ConnectionInfo> conns;
- if (!m_active) {
- return conns;
- }
-
- std::scoped_lock lock(m_user_mutex);
- for (auto& conn : m_connections) {
- if (conn->state() != NetworkConnection::kActive) {
- continue;
- }
- conns.emplace_back(conn->info());
- }
-
- return conns;
-}
-
-bool DispatcherBase::IsConnected() const {
- if (!m_active) {
- return false;
- }
-
- if (m_networkMode == NT_NET_MODE_LOCAL) {
- return true;
- }
-
- std::scoped_lock lock(m_user_mutex);
- for (auto& conn : m_connections) {
- if (conn->state() == NetworkConnection::kActive) {
- return true;
- }
- }
-
- return false;
-}
-
-unsigned int DispatcherBase::AddListener(
- std::function<void(const ConnectionNotification& event)> callback,
- bool immediate_notify) const {
- std::scoped_lock lock(m_user_mutex);
- unsigned int uid = m_notifier.Add(callback);
- // perform immediate notifications
- if (immediate_notify) {
- for (auto& conn : m_connections) {
- if (conn->state() != NetworkConnection::kActive) {
- continue;
- }
- m_notifier.NotifyConnection(true, conn->info(), uid);
- }
- }
- return uid;
-}
-
-unsigned int DispatcherBase::AddPolledListener(unsigned int poller_uid,
- bool immediate_notify) const {
- std::scoped_lock lock(m_user_mutex);
- unsigned int uid = m_notifier.AddPolled(poller_uid);
- // perform immediate notifications
- if (immediate_notify) {
- for (auto& conn : m_connections) {
- if (conn->state() != NetworkConnection::kActive) {
- continue;
- }
- m_notifier.NotifyConnection(true, conn->info(), uid);
- }
- }
- return uid;
-}
-
-void DispatcherBase::SetConnector(Connector connector) {
- std::scoped_lock lock(m_user_mutex);
- m_client_connector = std::move(connector);
-}
-
-void DispatcherBase::SetConnectorOverride(Connector connector) {
- std::scoped_lock lock(m_user_mutex);
- m_client_connector_override = std::move(connector);
-}
-
-void DispatcherBase::ClearConnectorOverride() {
- std::scoped_lock lock(m_user_mutex);
- m_client_connector_override = nullptr;
-}
-
-void DispatcherBase::DispatchThreadMain() {
- auto timeout_time = std::chrono::steady_clock::now();
-
- static const auto save_delta_time = std::chrono::seconds(1);
- auto next_save_time = timeout_time + save_delta_time;
-
- int count = 0;
-
- while (m_active) {
- // handle loop taking too long
- auto start = std::chrono::steady_clock::now();
- if (start > timeout_time) {
- timeout_time = start;
- }
-
- // wait for periodic or when flushed
- timeout_time += std::chrono::milliseconds(m_update_rate);
- std::unique_lock<wpi::mutex> flush_lock(m_flush_mutex);
- m_flush_cv.wait_until(flush_lock, timeout_time,
- [&] { return !m_active || m_do_flush; });
- m_do_flush = false;
- flush_lock.unlock();
- if (!m_active) {
- break; // in case we were woken up to terminate
- }
-
- // perform periodic persistent save
- if ((m_networkMode & NT_NET_MODE_SERVER) != 0 &&
- !m_persist_filename.empty() && start > next_save_time) {
- next_save_time += save_delta_time;
- // handle loop taking too long
- if (start > next_save_time) {
- next_save_time = start + save_delta_time;
- }
- const char* err = m_storage.SavePersistent(m_persist_filename, true);
- if (err) {
- WARNING("periodic persistent save: {}", err);
- }
- }
-
- {
- std::scoped_lock user_lock(m_user_mutex);
- bool reconnect = false;
-
- if (++count > 10) {
- DEBUG0("dispatch running {} connections", m_connections.size());
- count = 0;
- }
-
- for (auto& conn : m_connections) {
- // post outgoing messages if connection is active
- // only send keep-alives on client
- if (conn->state() == NetworkConnection::kActive) {
- conn->PostOutgoing((m_networkMode & NT_NET_MODE_CLIENT) != 0);
- }
-
- // if client, reconnect if connection died
- if ((m_networkMode & NT_NET_MODE_CLIENT) != 0 &&
- conn->state() == NetworkConnection::kDead) {
- reconnect = true;
- }
- }
- // reconnect if we disconnected (and a reconnect is not in progress)
- if (reconnect && !m_do_reconnect) {
- m_do_reconnect = true;
- m_reconnect_cv.notify_one();
- }
- }
- }
-}
-
-void DispatcherBase::QueueOutgoing(std::shared_ptr<Message> msg,
- INetworkConnection* only,
- INetworkConnection* except) {
- std::scoped_lock user_lock(m_user_mutex);
- for (auto& conn : m_connections) {
- if (conn.get() == except) {
- continue;
- }
- if (only && conn.get() != only) {
- continue;
- }
- auto state = conn->state();
- if (state != NetworkConnection::kSynchronized &&
- state != NetworkConnection::kActive) {
- continue;
- }
- conn->QueueOutgoing(msg);
- }
-}
-
-void DispatcherBase::ServerThreadMain() {
- if (m_server_acceptor->start() != 0) {
- m_active = false;
- m_networkMode = NT_NET_MODE_SERVER | NT_NET_MODE_FAILURE;
- return;
- }
- m_networkMode = NT_NET_MODE_SERVER;
- while (m_active) {
- auto stream = m_server_acceptor->accept();
- if (!stream) {
- m_active = false;
- return;
- }
- if (!m_active) {
- m_networkMode = NT_NET_MODE_NONE;
- return;
- }
- DEBUG0("server: client connection from {} port {}", stream->getPeerIP(),
- stream->getPeerPort());
-
- // add to connections list
- using namespace std::placeholders;
- auto conn = std::make_shared<NetworkConnection>(
- ++m_connections_uid, std::move(stream), m_notifier, m_logger,
- std::bind(&Dispatcher::ServerHandshake, this, _1, _2, _3), // NOLINT
- std::bind(&IStorage::GetMessageEntryType, &m_storage, _1)); // NOLINT
- conn->set_process_incoming(
- std::bind(&IStorage::ProcessIncoming, &m_storage, _1, _2, // NOLINT
- std::weak_ptr<NetworkConnection>(conn)));
- {
- std::scoped_lock lock(m_user_mutex);
- // reuse dead connection slots
- bool placed = false;
- for (auto& c : m_connections) {
- if (c->state() == NetworkConnection::kDead) {
- c = conn;
- placed = true;
- break;
- }
- }
- if (!placed) {
- m_connections.emplace_back(conn);
- }
- conn->Start();
- }
- }
- m_networkMode = NT_NET_MODE_NONE;
-}
-
-void DispatcherBase::ClientThreadMain() {
- while (m_active) {
- // sleep between retries
- std::this_thread::sleep_for(std::chrono::milliseconds(250));
- Connector connect;
-
- // get next server to connect to
- {
- std::scoped_lock lock(m_user_mutex);
- if (m_client_connector_override) {
- connect = m_client_connector_override;
- } else {
- if (!m_client_connector) {
- m_networkMode = NT_NET_MODE_CLIENT | NT_NET_MODE_FAILURE;
- continue;
- }
- connect = m_client_connector;
- }
- }
-
- // try to connect (with timeout)
- DEBUG0("{}", "client trying to connect");
- auto stream = connect();
- if (!stream) {
- m_networkMode = NT_NET_MODE_CLIENT | NT_NET_MODE_FAILURE;
- continue; // keep retrying
- }
- DEBUG0("{}", "client connected");
- m_networkMode = NT_NET_MODE_CLIENT;
-
- std::unique_lock lock(m_user_mutex);
- using namespace std::placeholders;
- auto conn = std::make_shared<NetworkConnection>(
- ++m_connections_uid, std::move(stream), m_notifier, m_logger,
- std::bind(&Dispatcher::ClientHandshake, this, _1, _2, _3), // NOLINT
- std::bind(&IStorage::GetMessageEntryType, &m_storage, _1)); // NOLINT
- conn->set_process_incoming(
- std::bind(&IStorage::ProcessIncoming, &m_storage, _1, _2, // NOLINT
- std::weak_ptr<NetworkConnection>(conn)));
- m_connections.resize(0); // disconnect any current
- m_connections.emplace_back(conn);
- conn->set_proto_rev(m_reconnect_proto_rev);
- conn->Start();
-
- // reconnect the next time starting with latest protocol revision
- m_reconnect_proto_rev = 0x0300;
-
- // block until told to reconnect
- m_do_reconnect = false;
- m_reconnect_cv.wait(lock, [&] { return !m_active || m_do_reconnect; });
- }
- m_networkMode = NT_NET_MODE_NONE;
-}
-
-bool DispatcherBase::ClientHandshake(
- NetworkConnection& conn, std::function<std::shared_ptr<Message>()> get_msg,
- std::function<void(wpi::span<std::shared_ptr<Message>>)> send_msgs) {
- // get identity
- std::string self_id;
- {
- std::scoped_lock lock(m_user_mutex);
- self_id = m_identity;
- }
-
- // send client hello
- DEBUG0("{}", "client: sending hello");
- auto msg = Message::ClientHello(self_id);
- send_msgs(wpi::span(&msg, 1));
-
- // wait for response
- msg = get_msg();
- if (!msg) {
- // disconnected, retry
- DEBUG0("{}", "client: server disconnected before first response");
- return false;
- }
-
- if (msg->Is(Message::kProtoUnsup)) {
- if (msg->id() == 0x0200) {
- ClientReconnect(0x0200);
- }
- return false;
- }
-
- bool new_server = true;
- if (conn.proto_rev() >= 0x0300) {
- // should be server hello; if not, disconnect.
- if (!msg->Is(Message::kServerHello)) {
- return false;
- }
- conn.set_remote_id(msg->str());
- if ((msg->flags() & 1) != 0) {
- new_server = false;
- }
- // get the next message
- msg = get_msg();
- }
-
- // receive initial assignments
- std::vector<std::shared_ptr<Message>> incoming;
- for (;;) {
- if (!msg) {
- // disconnected, retry
- DEBUG0("{}", "client: server disconnected during initial entries");
- return false;
- }
- DEBUG4("received init str={} id={} seq_num={}", msg->str(), msg->id(),
- msg->seq_num_uid());
- if (msg->Is(Message::kServerHelloDone)) {
- break;
- }
- // shouldn't receive a keep alive, but handle gracefully
- if (msg->Is(Message::kKeepAlive)) {
- msg = get_msg();
- continue;
- }
- if (!msg->Is(Message::kEntryAssign)) {
- // unexpected message
- DEBUG0(
- "client: received message ({}) other than entry assignment during "
- "initial handshake",
- msg->type());
- return false;
- }
- incoming.emplace_back(std::move(msg));
- // get the next message
- msg = get_msg();
- }
-
- // generate outgoing assignments
- NetworkConnection::Outgoing outgoing;
-
- m_storage.ApplyInitialAssignments(conn, incoming, new_server, &outgoing);
-
- if (conn.proto_rev() >= 0x0300) {
- outgoing.emplace_back(Message::ClientHelloDone());
- }
-
- if (!outgoing.empty()) {
- send_msgs(outgoing);
- }
-
- INFO("client: CONNECTED to server {} port {}", conn.stream().getPeerIP(),
- conn.stream().getPeerPort());
- return true;
-}
-
-bool DispatcherBase::ServerHandshake(
- NetworkConnection& conn, std::function<std::shared_ptr<Message>()> get_msg,
- std::function<void(wpi::span<std::shared_ptr<Message>>)> send_msgs) {
- // Wait for the client to send us a hello.
- auto msg = get_msg();
- if (!msg) {
- DEBUG0("{}", "server: client disconnected before sending hello");
- return false;
- }
- if (!msg->Is(Message::kClientHello)) {
- DEBUG0("{}", "server: client initial message was not client hello");
- return false;
- }
-
- // Check that the client requested version is not too high.
- unsigned int proto_rev = msg->id();
- if (proto_rev > 0x0300) {
- DEBUG0("{}", "server: client requested proto > 0x0300");
- auto toSend = Message::ProtoUnsup();
- send_msgs(wpi::span(&toSend, 1));
- return false;
- }
-
- if (proto_rev >= 0x0300) {
- conn.set_remote_id(msg->str());
- }
-
- // Set the proto version to the client requested version
- DEBUG0("server: client protocol {}", proto_rev);
- conn.set_proto_rev(proto_rev);
-
- // Send initial set of assignments
- NetworkConnection::Outgoing outgoing;
-
- // Start with server hello. TODO: initial connection flag
- if (proto_rev >= 0x0300) {
- std::scoped_lock lock(m_user_mutex);
- outgoing.emplace_back(Message::ServerHello(0u, m_identity));
- }
-
- // Get snapshot of initial assignments
- m_storage.GetInitialAssignments(conn, &outgoing);
-
- // Finish with server hello done
- outgoing.emplace_back(Message::ServerHelloDone());
-
- // Batch transmit
- DEBUG0("{}", "server: sending initial assignments");
- send_msgs(outgoing);
-
- // In proto rev 3.0 and later, the handshake concludes with a client hello
- // done message, so we can batch the assigns before marking the connection
- // active. In pre-3.0, we need to just immediately mark it active and hand
- // off control to the dispatcher to assign them as they arrive.
- if (proto_rev >= 0x0300) {
- // receive client initial assignments
- std::vector<std::shared_ptr<Message>> incoming;
- msg = get_msg();
- for (;;) {
- if (!msg) {
- // disconnected, retry
- DEBUG0("{}", "server: disconnected waiting for initial entries");
- return false;
- }
- if (msg->Is(Message::kClientHelloDone)) {
- break;
- }
- // shouldn't receive a keep alive, but handle gracefully
- if (msg->Is(Message::kKeepAlive)) {
- msg = get_msg();
- continue;
- }
- if (!msg->Is(Message::kEntryAssign)) {
- // unexpected message
- DEBUG0(
- "server: received message ({}) other than entry assignment during "
- "initial handshake",
- msg->type());
- return false;
- }
- incoming.push_back(msg);
- // get the next message (blocks)
- msg = get_msg();
- }
- for (auto& msg : incoming) {
- m_storage.ProcessIncoming(msg, &conn, std::weak_ptr<NetworkConnection>());
- }
- }
-
- INFO("server: client CONNECTED: {} port {}", conn.stream().getPeerIP(),
- conn.stream().getPeerPort());
- return true;
-}
-
-void DispatcherBase::ClientReconnect(unsigned int proto_rev) {
- if ((m_networkMode & NT_NET_MODE_SERVER) != 0) {
- return;
- }
- {
- std::scoped_lock lock(m_user_mutex);
- m_reconnect_proto_rev = proto_rev;
- m_do_reconnect = true;
- }
- m_reconnect_cv.notify_one();
-}
diff --git a/ntcore/src/main/native/cpp/Dispatcher.h b/ntcore/src/main/native/cpp/Dispatcher.h
deleted file mode 100644
index 51b8ec7..0000000
--- a/ntcore/src/main/native/cpp/Dispatcher.h
+++ /dev/null
@@ -1,149 +0,0 @@
-// 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.
-
-#ifndef NTCORE_DISPATCHER_H_
-#define NTCORE_DISPATCHER_H_
-
-#include <atomic>
-#include <functional>
-#include <memory>
-#include <string>
-#include <string_view>
-#include <thread>
-#include <utility>
-#include <vector>
-
-#include <wpi/condition_variable.h>
-#include <wpi/mutex.h>
-#include <wpi/span.h>
-
-#include "IDispatcher.h"
-#include "INetworkConnection.h"
-
-namespace wpi {
-class Logger;
-class NetworkAcceptor;
-class NetworkStream;
-} // namespace wpi
-
-namespace nt {
-
-class IConnectionNotifier;
-class IStorage;
-class NetworkConnection;
-
-class DispatcherBase : public IDispatcher {
- friend class DispatcherTest;
-
- public:
- using Connector = std::function<std::unique_ptr<wpi::NetworkStream>()>;
-
- DispatcherBase(IStorage& storage, IConnectionNotifier& notifier,
- wpi::Logger& logger);
- ~DispatcherBase() override;
-
- unsigned int GetNetworkMode() const;
- void StartLocal();
- void StartServer(std::string_view persist_filename,
- std::unique_ptr<wpi::NetworkAcceptor> acceptor);
- void StartClient();
- void Stop();
- void SetUpdateRate(double interval);
- void SetIdentity(std::string_view name);
- void Flush();
- std::vector<ConnectionInfo> GetConnections() const;
- bool IsConnected() const;
-
- unsigned int AddListener(
- std::function<void(const ConnectionNotification& event)> callback,
- bool immediate_notify) const;
- unsigned int AddPolledListener(unsigned int poller_uid,
- bool immediate_notify) const;
-
- void SetConnector(Connector connector);
- void SetConnectorOverride(Connector connector);
- void ClearConnectorOverride();
-
- bool active() const { return m_active; }
-
- DispatcherBase(const DispatcherBase&) = delete;
- DispatcherBase& operator=(const DispatcherBase&) = delete;
-
- private:
- void DispatchThreadMain();
- void ServerThreadMain();
- void ClientThreadMain();
-
- bool ClientHandshake(
- NetworkConnection& conn,
- std::function<std::shared_ptr<Message>()> get_msg,
- std::function<void(wpi::span<std::shared_ptr<Message>>)> send_msgs);
- bool ServerHandshake(
- NetworkConnection& conn,
- std::function<std::shared_ptr<Message>()> get_msg,
- std::function<void(wpi::span<std::shared_ptr<Message>>)> send_msgs);
-
- void ClientReconnect(unsigned int proto_rev = 0x0300);
-
- void QueueOutgoing(std::shared_ptr<Message> msg, INetworkConnection* only,
- INetworkConnection* except) override;
-
- IStorage& m_storage;
- IConnectionNotifier& m_notifier;
- unsigned int m_networkMode = NT_NET_MODE_NONE;
- std::string m_persist_filename;
- std::thread m_dispatch_thread;
- std::thread m_clientserver_thread;
-
- std::unique_ptr<wpi::NetworkAcceptor> m_server_acceptor;
- Connector m_client_connector_override;
- Connector m_client_connector;
- uint8_t m_connections_uid = 0;
-
- // Mutex for user-accessible items
- mutable wpi::mutex m_user_mutex;
- std::vector<std::shared_ptr<INetworkConnection>> m_connections;
- std::string m_identity;
-
- std::atomic_bool m_active; // set to false to terminate threads
- std::atomic_uint m_update_rate; // periodic dispatch update rate, in ms
-
- // Condition variable for forced dispatch wakeup (flush)
- wpi::mutex m_flush_mutex;
- wpi::condition_variable m_flush_cv;
- uint64_t m_last_flush = 0;
- bool m_do_flush = false;
-
- // Condition variable for client reconnect (uses user mutex)
- wpi::condition_variable m_reconnect_cv;
- unsigned int m_reconnect_proto_rev = 0x0300;
- bool m_do_reconnect = true;
-
- protected:
- wpi::Logger& m_logger;
-};
-
-class Dispatcher : public DispatcherBase {
- friend class DispatcherTest;
-
- public:
- Dispatcher(IStorage& storage, IConnectionNotifier& notifier,
- wpi::Logger& logger)
- : DispatcherBase(storage, notifier, logger) {}
-
- void StartServer(std::string_view persist_filename,
- const char* listen_address, unsigned int port);
-
- void SetServer(const char* server_name, unsigned int port);
- void SetServer(
- wpi::span<const std::pair<std::string_view, unsigned int>> servers);
- void SetServerTeam(unsigned int team, unsigned int port);
-
- void SetServerOverride(const char* server_name, unsigned int port);
- void ClearServerOverride();
-};
-
-} // namespace nt
-
-#endif // NTCORE_DISPATCHER_H_
diff --git a/ntcore/src/main/native/cpp/DsClient.cpp b/ntcore/src/main/native/cpp/DsClient.cpp
deleted file mode 100644
index 9ed1850..0000000
--- a/ntcore/src/main/native/cpp/DsClient.cpp
+++ /dev/null
@@ -1,177 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "DsClient.h"
-
-#include <wpi/SmallString.h>
-#include <wpi/StringExtras.h>
-#include <wpi/TCPConnector.h>
-#include <wpi/raw_ostream.h>
-#include <wpi/raw_socket_istream.h>
-
-#include "Dispatcher.h"
-#include "Log.h"
-
-using namespace nt;
-
-class DsClient::Thread : public wpi::SafeThread {
- public:
- Thread(Dispatcher& dispatcher, wpi::Logger& logger, unsigned int port)
- : m_dispatcher(dispatcher), m_logger(logger), m_port(port) {}
-
- void Main() override;
-
- Dispatcher& m_dispatcher;
- wpi::Logger& m_logger;
- unsigned int m_port;
- std::unique_ptr<wpi::NetworkStream> m_stream;
-};
-
-DsClient::DsClient(Dispatcher& dispatcher, wpi::Logger& logger)
- : m_dispatcher(dispatcher), m_logger(logger) {}
-
-void DsClient::Start(unsigned int port) {
- auto thr = m_owner.GetThread();
- if (!thr) {
- m_owner.Start(m_dispatcher, m_logger, port);
- } else {
- thr->m_port = port;
- }
-}
-
-void DsClient::Stop() {
- {
- // Close the stream so the read (if any) terminates.
- auto thr = m_owner.GetThread();
- if (thr) {
- thr->m_active = false;
- if (thr->m_stream) {
- thr->m_stream->close();
- }
- }
- }
- m_owner.Stop();
-}
-
-void DsClient::Thread::Main() {
- unsigned int oldip = 0;
- wpi::Logger nolog; // to silence log messages from TCPConnector
-
- while (m_active) {
- // wait for periodic reconnect or termination
- auto timeout_time =
- std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
- unsigned int port;
- {
- std::unique_lock lock(m_mutex);
- m_cond.wait_until(lock, timeout_time, [&] { return !m_active; });
- port = m_port;
- }
- if (!m_active) {
- goto done;
- }
-
- // Try to connect to DS on the local machine
- m_stream = wpi::TCPConnector::connect("127.0.0.1", 1742, nolog, 1);
- if (!m_active) {
- goto done;
- }
- if (!m_stream) {
- continue;
- }
-
- DEBUG3("{}", "connected to DS");
- wpi::raw_socket_istream is(*m_stream);
-
- while (m_active && !is.has_error()) {
- // Read JSON "{...}". This is very limited, does not handle quoted "}" or
- // nested {}, but is sufficient for this purpose.
- wpi::SmallString<128> json;
- char ch;
-
- // Throw away characters until {
- do {
- is.read(ch);
- if (is.has_error()) {
- break;
- }
- if (!m_active) {
- goto done;
- }
- } while (ch != '{');
- json += '{';
-
- if (is.has_error()) {
- m_stream = nullptr;
- break;
- }
-
- // Read characters until }
- do {
- is.read(ch);
- if (is.has_error()) {
- break;
- }
- if (!m_active) {
- goto done;
- }
- json += ch;
- } while (ch != '}');
-
- if (is.has_error()) {
- m_stream = nullptr;
- break;
- }
- DEBUG3("json={}", json);
-
- // Look for "robotIP":12345, and get 12345 portion
- size_t pos = json.find("\"robotIP\"");
- if (pos == std::string_view::npos) {
- continue; // could not find?
- }
- pos += 9;
- pos = json.find(':', pos);
- if (pos == std::string_view::npos) {
- continue; // could not find?
- }
- size_t endpos = json.find_first_not_of("0123456789", pos + 1);
- DEBUG3("found robotIP={}", wpi::slice(json, pos + 1, endpos));
-
- // Parse into number
- unsigned int ip = 0;
- if (auto v = wpi::parse_integer<unsigned int>(
- wpi::slice(json, pos + 1, endpos), 10)) {
- ip = v.value();
- } else {
- continue; // error
- }
-
- // If zero, clear the server override
- if (ip == 0) {
- m_dispatcher.ClearServerOverride();
- oldip = 0;
- continue;
- }
-
- // If unchanged, don't reconnect
- if (ip == oldip) {
- continue;
- }
- oldip = ip;
-
- // Convert number into dotted quad
- auto newip = fmt::format("{}.{}.{}.{}", (ip >> 24) & 0xff,
- (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff);
- INFO("client: DS overriding server IP to {}", newip);
- m_dispatcher.SetServerOverride(newip.c_str(), port);
- }
-
- // We disconnected from the DS, clear the server override
- m_dispatcher.ClearServerOverride();
- oldip = 0;
- }
-
-done:
- m_dispatcher.ClearServerOverride();
-}
diff --git a/ntcore/src/main/native/cpp/DsClient.h b/ntcore/src/main/native/cpp/DsClient.h
deleted file mode 100644
index 73fc3d3..0000000
--- a/ntcore/src/main/native/cpp/DsClient.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// 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.
-
-#ifndef NTCORE_DSCLIENT_H_
-#define NTCORE_DSCLIENT_H_
-
-#include <wpi/SafeThread.h>
-
-#include "Log.h"
-
-namespace nt {
-
-class Dispatcher;
-
-class DsClient {
- public:
- DsClient(Dispatcher& dispatcher, wpi::Logger& logger);
- ~DsClient() = default;
-
- void Start(unsigned int port);
- void Stop();
-
- private:
- class Thread;
- wpi::SafeThreadOwner<Thread> m_owner;
- Dispatcher& m_dispatcher;
- wpi::Logger& m_logger;
-};
-
-} // namespace nt
-
-#endif // NTCORE_DSCLIENT_H_
diff --git a/ntcore/src/main/native/cpp/EntryNotifier.cpp b/ntcore/src/main/native/cpp/EntryNotifier.cpp
deleted file mode 100644
index 6f251a3..0000000
--- a/ntcore/src/main/native/cpp/EntryNotifier.cpp
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "EntryNotifier.h"
-
-#include <wpi/StringExtras.h>
-
-#include "Log.h"
-
-using namespace nt;
-
-EntryNotifier::EntryNotifier(int inst, wpi::Logger& logger)
- : m_inst(inst), m_logger(logger) {
- m_local_notifiers = false;
-}
-
-void EntryNotifier::Start() {
- DoStart(m_inst);
-}
-
-bool EntryNotifier::local_notifiers() const {
- return m_local_notifiers;
-}
-
-bool impl::EntryNotifierThread::Matches(const EntryListenerData& listener,
- const EntryNotification& data) {
- if (!data.value) {
- return false;
- }
-
- // Flags must be within requested flag set for this listener.
- // Because assign messages can result in both a value and flags update,
- // we handle that case specially.
- unsigned int listen_flags =
- listener.flags & ~(NT_NOTIFY_IMMEDIATE | NT_NOTIFY_LOCAL);
- unsigned int flags = data.flags & ~(NT_NOTIFY_IMMEDIATE | NT_NOTIFY_LOCAL);
- unsigned int assign_both = NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS;
- if ((flags & assign_both) == assign_both) {
- if ((listen_flags & assign_both) == 0) {
- return false;
- }
- listen_flags &= ~assign_both;
- flags &= ~assign_both;
- }
- if ((flags & ~listen_flags) != 0) {
- return false;
- }
-
- // must match local id or prefix
- if (listener.entry != 0 && data.entry != listener.entry) {
- return false;
- }
- if (listener.entry == 0 && !wpi::starts_with(data.name, listener.prefix)) {
- return false;
- }
-
- return true;
-}
-
-unsigned int EntryNotifier::Add(
- std::function<void(const EntryNotification& event)> callback,
- std::string_view prefix, unsigned int flags) {
- if ((flags & NT_NOTIFY_LOCAL) != 0) {
- m_local_notifiers = true;
- }
- return DoAdd(callback, prefix, flags);
-}
-
-unsigned int EntryNotifier::Add(
- std::function<void(const EntryNotification& event)> callback,
- unsigned int local_id, unsigned int flags) {
- if ((flags & NT_NOTIFY_LOCAL) != 0) {
- m_local_notifiers = true;
- }
- return DoAdd(callback, Handle(m_inst, local_id, Handle::kEntry), flags);
-}
-
-unsigned int EntryNotifier::AddPolled(unsigned int poller_uid,
- std::string_view prefix,
- unsigned int flags) {
- if ((flags & NT_NOTIFY_LOCAL) != 0) {
- m_local_notifiers = true;
- }
- return DoAdd(poller_uid, prefix, flags);
-}
-
-unsigned int EntryNotifier::AddPolled(unsigned int poller_uid,
- unsigned int local_id,
- unsigned int flags) {
- if ((flags & NT_NOTIFY_LOCAL) != 0) {
- m_local_notifiers = true;
- }
- return DoAdd(poller_uid, Handle(m_inst, local_id, Handle::kEntry), flags);
-}
-
-void EntryNotifier::NotifyEntry(unsigned int local_id, std::string_view name,
- std::shared_ptr<Value> value,
- unsigned int flags,
- unsigned int only_listener) {
- // optimization: don't generate needless local queue entries if we have
- // no local listeners (as this is a common case on the server side)
- if ((flags & NT_NOTIFY_LOCAL) != 0 && !m_local_notifiers) {
- return;
- }
- DEBUG0("notifying '{}' (local={}), flags={}", name, local_id, flags);
- Send(only_listener, 0, Handle(m_inst, local_id, Handle::kEntry).handle(),
- name, value, flags);
-}
diff --git a/ntcore/src/main/native/cpp/EntryNotifier.h b/ntcore/src/main/native/cpp/EntryNotifier.h
deleted file mode 100644
index bbe2172..0000000
--- a/ntcore/src/main/native/cpp/EntryNotifier.h
+++ /dev/null
@@ -1,112 +0,0 @@
-// 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.
-
-#ifndef NTCORE_ENTRYNOTIFIER_H_
-#define NTCORE_ENTRYNOTIFIER_H_
-
-#include <atomic>
-#include <memory>
-#include <string>
-#include <string_view>
-#include <utility>
-
-#include <wpi/CallbackManager.h>
-
-#include "Handle.h"
-#include "IEntryNotifier.h"
-#include "ntcore_cpp.h"
-
-namespace wpi {
-class Logger;
-} // namespace wpi
-
-namespace nt {
-
-namespace impl {
-
-struct EntryListenerData
- : public wpi::CallbackListenerData<
- std::function<void(const EntryNotification& event)>> {
- EntryListenerData() = default;
- EntryListenerData(
- std::function<void(const EntryNotification& event)> callback_,
- std::string_view prefix_, unsigned int flags_)
- : CallbackListenerData(callback_), prefix(prefix_), flags(flags_) {}
- EntryListenerData(
- std::function<void(const EntryNotification& event)> callback_,
- NT_Entry entry_, unsigned int flags_)
- : CallbackListenerData(callback_), entry(entry_), flags(flags_) {}
- EntryListenerData(unsigned int poller_uid_, std::string_view prefix_,
- unsigned int flags_)
- : CallbackListenerData(poller_uid_), prefix(prefix_), flags(flags_) {}
- EntryListenerData(unsigned int poller_uid_, NT_Entry entry_,
- unsigned int flags_)
- : CallbackListenerData(poller_uid_), entry(entry_), flags(flags_) {}
-
- std::string prefix;
- NT_Entry entry = 0;
- unsigned int flags;
-};
-
-class EntryNotifierThread
- : public wpi::CallbackThread<EntryNotifierThread, EntryNotification,
- EntryListenerData> {
- public:
- EntryNotifierThread(std::function<void()> on_start,
- std::function<void()> on_exit, int inst)
- : CallbackThread(std::move(on_start), std::move(on_exit)), m_inst(inst) {}
-
- bool Matches(const EntryListenerData& listener,
- const EntryNotification& data);
-
- void SetListener(EntryNotification* data, unsigned int listener_uid) {
- data->listener =
- Handle(m_inst, listener_uid, Handle::kEntryListener).handle();
- }
-
- void DoCallback(std::function<void(const EntryNotification& event)> callback,
- const EntryNotification& data) {
- callback(data);
- }
-
- int m_inst;
-};
-
-} // namespace impl
-
-class EntryNotifier
- : public IEntryNotifier,
- public wpi::CallbackManager<EntryNotifier, impl::EntryNotifierThread> {
- friend class EntryNotifierTest;
- friend class wpi::CallbackManager<EntryNotifier, impl::EntryNotifierThread>;
-
- public:
- explicit EntryNotifier(int inst, wpi::Logger& logger);
-
- void Start();
-
- bool local_notifiers() const override;
-
- unsigned int Add(std::function<void(const EntryNotification& event)> callback,
- std::string_view prefix, unsigned int flags) override;
- unsigned int Add(std::function<void(const EntryNotification& event)> callback,
- unsigned int local_id, unsigned int flags) override;
- unsigned int AddPolled(unsigned int poller_uid, std::string_view prefix,
- unsigned int flags) override;
- unsigned int AddPolled(unsigned int poller_uid, unsigned int local_id,
- unsigned int flags) override;
-
- void NotifyEntry(unsigned int local_id, std::string_view name,
- std::shared_ptr<Value> value, unsigned int flags,
- unsigned int only_listener = UINT_MAX) override;
-
- private:
- int m_inst;
- wpi::Logger& m_logger;
- std::atomic_bool m_local_notifiers;
-};
-
-} // namespace nt
-
-#endif // NTCORE_ENTRYNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/Handle.h b/ntcore/src/main/native/cpp/Handle.h
index df12381..0eb9041 100644
--- a/ntcore/src/main/native/cpp/Handle.h
+++ b/ntcore/src/main/native/cpp/Handle.h
@@ -2,8 +2,7 @@
// 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.
-#ifndef NTCORE_HANDLE_H_
-#define NTCORE_HANDLE_H_
+#pragma once
#include <wpi/Synchronization.h>
@@ -19,17 +18,19 @@
class Handle {
public:
enum Type {
- kConnectionListener = wpi::kHandleTypeNTBase,
- kConnectionListenerPoller,
+ kListener = wpi::kHandleTypeNTBase,
+ kListenerPoller,
kEntry,
- kEntryListener,
- kEntryListenerPoller,
kInstance,
- kLogger,
- kLoggerPoller,
- kRpcCall,
- kRpcCallPoller
+ kDataLogger,
+ kConnectionDataLogger,
+ kMultiSubscriber,
+ kTopic,
+ kSubscriber,
+ kPublisher,
+ kTypeMax
};
+ static_assert(kTypeMax <= wpi::kHandleTypeHALBase);
enum { kIndexMax = 0xfffff };
explicit Handle(NT_Handle handle) : m_handle(handle) {}
@@ -46,7 +47,9 @@
(index & 0xfffff);
}
- int GetIndex() const { return static_cast<int>(m_handle) & 0xfffff; }
+ unsigned int GetIndex() const {
+ return static_cast<unsigned int>(m_handle) & 0xfffff;
+ }
Type GetType() const {
return static_cast<Type>((static_cast<int>(m_handle) >> 24) & 0x7f);
}
@@ -60,5 +63,3 @@
};
} // namespace nt
-
-#endif // NTCORE_HANDLE_H_
diff --git a/ntcore/src/main/native/cpp/HandleMap.h b/ntcore/src/main/native/cpp/HandleMap.h
new file mode 100644
index 0000000..03e73f9
--- /dev/null
+++ b/ntcore/src/main/native/cpp/HandleMap.h
@@ -0,0 +1,54 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <memory>
+#include <utility>
+
+#include <wpi/UidVector.h>
+
+#include "Handle.h"
+
+namespace nt {
+
+// Utility wrapper class for our UidVectors
+template <typename T, size_t Size>
+class HandleMap : public wpi::UidVector<std::unique_ptr<T>, Size> {
+ public:
+ template <typename... Args>
+ T* Add(int inst, Args&&... args) {
+ auto i = this->emplace_back();
+ auto& it = (*this)[i];
+ it = std::make_unique<T>(Handle(inst, i, T::kType),
+ std::forward<Args>(args)...);
+ return it.get();
+ }
+
+ std::unique_ptr<T> Remove(NT_Handle handle) {
+ Handle h{handle};
+ if (!h.IsType(T::kType)) {
+ return {};
+ }
+ unsigned int i = h.GetIndex();
+ if (i >= this->size()) {
+ return {};
+ }
+ return this->erase(i);
+ }
+
+ T* Get(NT_Handle handle) {
+ Handle h{handle};
+ if (!h.IsType(T::kType)) {
+ return {};
+ }
+ unsigned int i = h.GetIndex();
+ if (i >= this->size()) {
+ return {};
+ }
+ return (*this)[i].get();
+ }
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/IConnectionList.h b/ntcore/src/main/native/cpp/IConnectionList.h
new file mode 100644
index 0000000..a4a59e3
--- /dev/null
+++ b/ntcore/src/main/native/cpp/IConnectionList.h
@@ -0,0 +1,24 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <vector>
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class IConnectionList {
+ public:
+ virtual ~IConnectionList() = default;
+
+ virtual int AddConnection(const ConnectionInfo& info) = 0;
+ virtual void RemoveConnection(int handle) = 0;
+ virtual void ClearConnections() = 0;
+ virtual std::vector<ConnectionInfo> GetConnections() const = 0;
+ virtual bool IsConnected() const = 0;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/IConnectionNotifier.h b/ntcore/src/main/native/cpp/IConnectionNotifier.h
deleted file mode 100644
index 5983866..0000000
--- a/ntcore/src/main/native/cpp/IConnectionNotifier.h
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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.
-
-#ifndef NTCORE_ICONNECTIONNOTIFIER_H_
-#define NTCORE_ICONNECTIONNOTIFIER_H_
-
-#include <climits>
-
-#include "ntcore_cpp.h"
-
-namespace nt {
-
-class IConnectionNotifier {
- public:
- IConnectionNotifier() = default;
- IConnectionNotifier(const IConnectionNotifier&) = delete;
- IConnectionNotifier& operator=(const IConnectionNotifier&) = delete;
- virtual ~IConnectionNotifier() = default;
- virtual unsigned int Add(
- std::function<void(const ConnectionNotification& event)> callback) = 0;
- virtual unsigned int AddPolled(unsigned int poller_uid) = 0;
- virtual void NotifyConnection(bool connected, const ConnectionInfo& conn_info,
- unsigned int only_listener = UINT_MAX) = 0;
-};
-
-} // namespace nt
-
-#endif // NTCORE_ICONNECTIONNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/IDispatcher.h b/ntcore/src/main/native/cpp/IDispatcher.h
deleted file mode 100644
index 4cf8a5f..0000000
--- a/ntcore/src/main/native/cpp/IDispatcher.h
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.
-
-#ifndef NTCORE_IDISPATCHER_H_
-#define NTCORE_IDISPATCHER_H_
-
-#include <memory>
-
-#include "Message.h"
-
-namespace nt {
-
-class INetworkConnection;
-
-// Interface for generation of outgoing messages to break a dependency loop
-// between Storage and Dispatcher.
-class IDispatcher {
- public:
- IDispatcher() = default;
- IDispatcher(const IDispatcher&) = delete;
- IDispatcher& operator=(const IDispatcher&) = delete;
- virtual ~IDispatcher() = default;
- virtual void QueueOutgoing(std::shared_ptr<Message> msg,
- INetworkConnection* only,
- INetworkConnection* except) = 0;
-};
-
-} // namespace nt
-
-#endif // NTCORE_IDISPATCHER_H_
diff --git a/ntcore/src/main/native/cpp/IEntryNotifier.h b/ntcore/src/main/native/cpp/IEntryNotifier.h
deleted file mode 100644
index b3f91b2..0000000
--- a/ntcore/src/main/native/cpp/IEntryNotifier.h
+++ /dev/null
@@ -1,43 +0,0 @@
-// 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.
-
-#ifndef NTCORE_IENTRYNOTIFIER_H_
-#define NTCORE_IENTRYNOTIFIER_H_
-
-#include <climits>
-#include <memory>
-#include <string_view>
-
-#include "ntcore_cpp.h"
-
-namespace nt {
-
-class IEntryNotifier {
- public:
- IEntryNotifier() = default;
- IEntryNotifier(const IEntryNotifier&) = delete;
- IEntryNotifier& operator=(const IEntryNotifier&) = delete;
- virtual ~IEntryNotifier() = default;
- virtual bool local_notifiers() const = 0;
-
- virtual unsigned int Add(
- std::function<void(const EntryNotification& event)> callback,
- std::string_view prefix, unsigned int flags) = 0;
- virtual unsigned int Add(
- std::function<void(const EntryNotification& event)> callback,
- unsigned int local_id, unsigned int flags) = 0;
- virtual unsigned int AddPolled(unsigned int poller_uid,
- std::string_view prefix,
- unsigned int flags) = 0;
- virtual unsigned int AddPolled(unsigned int poller_uid, unsigned int local_id,
- unsigned int flags) = 0;
-
- virtual void NotifyEntry(unsigned int local_id, std::string_view name,
- std::shared_ptr<Value> value, unsigned int flags,
- unsigned int only_listener = UINT_MAX) = 0;
-};
-
-} // namespace nt
-
-#endif // NTCORE_IENTRYNOTIFIER_H_
diff --git a/ntcore/src/main/native/cpp/IListenerStorage.h b/ntcore/src/main/native/cpp/IListenerStorage.h
new file mode 100644
index 0000000..82d7432
--- /dev/null
+++ b/ntcore/src/main/native/cpp/IListenerStorage.h
@@ -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.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <span>
+#include <string_view>
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class IListenerStorage {
+ public:
+ // Return false if event should not be issued (final check).
+ // This is called only during Notify() processing.
+ using FinishEventFunc = std::function<bool(unsigned int mask, Event* event)>;
+
+ virtual ~IListenerStorage() = default;
+
+ virtual void Activate(NT_Listener listener, unsigned int mask,
+ FinishEventFunc finishEvent = {}) = 0;
+
+ // If handles is not empty, notifies ONLY those listeners
+ virtual void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ std::span<ConnectionInfo const* const> infos) = 0;
+ virtual void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ std::span<const TopicInfo> infos) = 0;
+ virtual void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ NT_Topic topic, NT_Handle subentry,
+ const Value& value) = 0;
+ virtual void Notify(unsigned int flags, unsigned int level,
+ std::string_view filename, unsigned int line,
+ std::string_view message) = 0;
+ virtual void NotifyTimeSync(std::span<const NT_Listener> handles,
+ unsigned int flags, int64_t serverTimeOffset,
+ int64_t rtt2, bool valid) = 0;
+
+ void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ const ConnectionInfo* info) {
+ Notify(handles, flags, {&info, 1});
+ }
+ void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ const TopicInfo& info) {
+ Notify(handles, flags, {&info, 1});
+ }
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/INetworkClient.h b/ntcore/src/main/native/cpp/INetworkClient.h
new file mode 100644
index 0000000..986f43a
--- /dev/null
+++ b/ntcore/src/main/native/cpp/INetworkClient.h
@@ -0,0 +1,29 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <span>
+#include <string>
+#include <utility>
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class INetworkClient {
+ public:
+ virtual ~INetworkClient() = default;
+
+ virtual void SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers) = 0;
+
+ virtual void StartDSClient(unsigned int port) = 0;
+ virtual void StopDSClient() = 0;
+
+ virtual void FlushLocal() = 0;
+ virtual void Flush() = 0;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/INetworkConnection.h b/ntcore/src/main/native/cpp/INetworkConnection.h
deleted file mode 100644
index 94e9bb1..0000000
--- a/ntcore/src/main/native/cpp/INetworkConnection.h
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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.
-
-#ifndef NTCORE_INETWORKCONNECTION_H_
-#define NTCORE_INETWORKCONNECTION_H_
-
-#include <memory>
-
-#include "Message.h"
-#include "ntcore_cpp.h"
-
-namespace nt {
-
-class INetworkConnection {
- public:
- enum State { kCreated, kInit, kHandshake, kSynchronized, kActive, kDead };
-
- INetworkConnection() = default;
- INetworkConnection(const INetworkConnection&) = delete;
- INetworkConnection& operator=(const INetworkConnection&) = delete;
- virtual ~INetworkConnection() = default;
-
- virtual ConnectionInfo info() const = 0;
-
- virtual void QueueOutgoing(std::shared_ptr<Message> msg) = 0;
- virtual void PostOutgoing(bool keep_alive) = 0;
-
- virtual unsigned int proto_rev() const = 0;
- virtual void set_proto_rev(unsigned int proto_rev) = 0;
-
- virtual State state() const = 0;
- virtual void set_state(State state) = 0;
-};
-
-} // namespace nt
-
-#endif // NTCORE_INETWORKCONNECTION_H_
diff --git a/ntcore/src/main/native/cpp/IRpcServer.h b/ntcore/src/main/native/cpp/IRpcServer.h
deleted file mode 100644
index aa16084..0000000
--- a/ntcore/src/main/native/cpp/IRpcServer.h
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.
-
-#ifndef NTCORE_IRPCSERVER_H_
-#define NTCORE_IRPCSERVER_H_
-
-#include <memory>
-#include <string_view>
-
-#include "Message.h"
-#include "ntcore_cpp.h"
-
-namespace nt {
-
-class IRpcServer {
- public:
- using SendResponseFunc = std::function<void(std::string_view result)>;
-
- IRpcServer() = default;
- IRpcServer(const IRpcServer&) = delete;
- IRpcServer& operator=(const IRpcServer&) = delete;
- virtual ~IRpcServer() = default;
-
- virtual void RemoveRpc(unsigned int rpc_uid) = 0;
-
- virtual void ProcessRpc(unsigned int local_id, unsigned int call_uid,
- std::string_view name, std::string_view params,
- const ConnectionInfo& conn,
- SendResponseFunc send_response,
- unsigned int rpc_uid) = 0;
-};
-
-} // namespace nt
-
-#endif // NTCORE_IRPCSERVER_H_
diff --git a/ntcore/src/main/native/cpp/IStorage.h b/ntcore/src/main/native/cpp/IStorage.h
deleted file mode 100644
index 795d032..0000000
--- a/ntcore/src/main/native/cpp/IStorage.h
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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.
-
-#ifndef NTCORE_ISTORAGE_H_
-#define NTCORE_ISTORAGE_H_
-
-#include <functional>
-#include <memory>
-#include <string_view>
-#include <vector>
-
-#include <wpi/span.h>
-
-#include "Message.h"
-#include "ntcore_cpp.h"
-
-namespace nt {
-
-class IDispatcher;
-class INetworkConnection;
-
-class IStorage {
- public:
- IStorage() = default;
- IStorage(const IStorage&) = delete;
- IStorage& operator=(const IStorage&) = delete;
- virtual ~IStorage() = default;
-
- // Accessors required by Dispatcher. An interface is used for
- // generation of outgoing messages to break a dependency loop between
- // Storage and Dispatcher.
- virtual void SetDispatcher(IDispatcher* dispatcher, bool server) = 0;
- virtual void ClearDispatcher() = 0;
-
- // Required for wire protocol 2.0 to get the entry type of an entry when
- // receiving entry updates (because the length/type is not provided in the
- // message itself). Not used in wire protocol 3.0.
- virtual NT_Type GetMessageEntryType(unsigned int id) const = 0;
-
- virtual void ProcessIncoming(std::shared_ptr<Message> msg,
- INetworkConnection* conn,
- std::weak_ptr<INetworkConnection> conn_weak) = 0;
- virtual void GetInitialAssignments(
- INetworkConnection& conn,
- std::vector<std::shared_ptr<Message>>* msgs) = 0;
- virtual void ApplyInitialAssignments(
- INetworkConnection& conn, wpi::span<std::shared_ptr<Message>> msgs,
- bool new_server, std::vector<std::shared_ptr<Message>>* out_msgs) = 0;
-
- // Filename-based save/load functions. Used both by periodic saves and
- // accessible directly via the user API.
- virtual const char* SavePersistent(std::string_view filename,
- bool periodic) const = 0;
- virtual const char* LoadPersistent(
- std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn) = 0;
-};
-
-} // namespace nt
-
-#endif // NTCORE_ISTORAGE_H_
diff --git a/ntcore/src/main/native/cpp/InstanceImpl.cpp b/ntcore/src/main/native/cpp/InstanceImpl.cpp
index 8d59a0b..b34db73 100644
--- a/ntcore/src/main/native/cpp/InstanceImpl.cpp
+++ b/ntcore/src/main/native/cpp/InstanceImpl.cpp
@@ -13,22 +13,16 @@
using namespace std::placeholders;
InstanceImpl::InstanceImpl(int inst)
- : logger_impl(inst),
- logger(
- std::bind(&LoggerImpl::Log, &logger_impl, _1, _2, _3, _4)), // NOLINT
- connection_notifier(inst),
- entry_notifier(inst, logger),
- rpc_server(inst, logger),
- storage(entry_notifier, rpc_server, logger),
- dispatcher(storage, connection_notifier, logger),
- ds_client(dispatcher, logger) {
+ : listenerStorage{inst},
+ logger_impl{listenerStorage},
+ logger{
+ std::bind(&LoggerImpl::Log, &logger_impl, _1, _2, _3, _4)}, // NOLINT
+ connectionList{inst, listenerStorage},
+ localStorage{inst, listenerStorage, logger},
+ m_inst{inst} {
logger.set_min_level(logger_impl.GetMinLevel());
}
-InstanceImpl::~InstanceImpl() {
- logger.SetLogger(nullptr);
-}
-
InstanceImpl* InstanceImpl::GetDefault() {
return Get(GetDefaultIndex());
}
@@ -83,7 +77,164 @@
return;
}
- InstanceImpl* ptr = nullptr;
- s_instances[inst].exchange(ptr);
- delete ptr;
+ delete s_instances[inst].exchange(nullptr);
+}
+
+void InstanceImpl::StartLocal() {
+ std::scoped_lock lock{m_mutex};
+ if (networkMode != NT_NET_MODE_NONE) {
+ return;
+ }
+ networkMode = NT_NET_MODE_LOCAL;
+}
+
+void InstanceImpl::StopLocal() {
+ std::scoped_lock lock{m_mutex};
+ if ((networkMode & NT_NET_MODE_LOCAL) == 0) {
+ return;
+ }
+ networkMode = NT_NET_MODE_NONE;
+}
+
+void InstanceImpl::StartServer(std::string_view persistFilename,
+ std::string_view listenAddress,
+ unsigned int port3, unsigned int port4) {
+ std::scoped_lock lock{m_mutex};
+ if (networkMode != NT_NET_MODE_NONE) {
+ return;
+ }
+ m_networkServer = std::make_shared<NetworkServer>(
+ persistFilename, listenAddress, port3, port4, localStorage,
+ connectionList, logger, [this] {
+ std::scoped_lock lock{m_mutex};
+ networkMode &= ~NT_NET_MODE_STARTING;
+ });
+ networkMode = NT_NET_MODE_SERVER | NT_NET_MODE_STARTING;
+ listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, 0, 0, true);
+ m_serverTimeOffset = 0;
+ m_rtt2 = 0;
+}
+
+void InstanceImpl::StopServer() {
+ std::shared_ptr<NetworkServer> server;
+ {
+ std::scoped_lock lock{m_mutex};
+ if ((networkMode & NT_NET_MODE_SERVER) == 0) {
+ return;
+ }
+ server = std::move(m_networkServer);
+ networkMode = NT_NET_MODE_NONE;
+ listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, 0, 0, false);
+ m_serverTimeOffset.reset();
+ m_rtt2 = 0;
+ }
+}
+
+void InstanceImpl::StartClient3(std::string_view identity) {
+ std::scoped_lock lock{m_mutex};
+ if (networkMode != NT_NET_MODE_NONE) {
+ return;
+ }
+ m_networkClient = std::make_shared<NetworkClient3>(
+ m_inst, identity, localStorage, connectionList, logger);
+ if (!m_servers.empty()) {
+ m_networkClient->SetServers(m_servers);
+ }
+ networkMode = NT_NET_MODE_CLIENT3;
+}
+
+void InstanceImpl::StartClient4(std::string_view identity) {
+ std::scoped_lock lock{m_mutex};
+ if (networkMode != NT_NET_MODE_NONE) {
+ return;
+ }
+ m_networkClient = std::make_shared<NetworkClient>(
+ m_inst, identity, localStorage, connectionList, logger,
+ [this](int64_t serverTimeOffset, int64_t rtt2, bool valid) {
+ std::scoped_lock lock{m_mutex};
+ listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, serverTimeOffset,
+ rtt2, valid);
+ if (valid) {
+ m_serverTimeOffset = serverTimeOffset;
+ m_rtt2 = rtt2;
+ } else {
+ m_serverTimeOffset.reset();
+ m_rtt2 = 0;
+ }
+ });
+ if (!m_servers.empty()) {
+ m_networkClient->SetServers(m_servers);
+ }
+ networkMode = NT_NET_MODE_CLIENT4;
+}
+
+void InstanceImpl::StopClient() {
+ std::shared_ptr<INetworkClient> client;
+ {
+ std::scoped_lock lock{m_mutex};
+ if ((networkMode & (NT_NET_MODE_CLIENT3 | NT_NET_MODE_CLIENT4)) == 0) {
+ return;
+ }
+ client = std::move(m_networkClient);
+ networkMode = NT_NET_MODE_NONE;
+ }
+ client.reset();
+ {
+ std::scoped_lock lock{m_mutex};
+ listenerStorage.NotifyTimeSync({}, NT_EVENT_TIMESYNC, 0, 0, false);
+ m_serverTimeOffset.reset();
+ m_rtt2 = 0;
+ }
+}
+
+void InstanceImpl::SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers) {
+ std::scoped_lock lock{m_mutex};
+ m_servers = {servers.begin(), servers.end()};
+ if (m_networkClient) {
+ m_networkClient->SetServers(servers);
+ }
+}
+
+std::shared_ptr<NetworkServer> InstanceImpl::GetServer() {
+ std::scoped_lock lock{m_mutex};
+ return m_networkServer;
+}
+
+std::shared_ptr<INetworkClient> InstanceImpl::GetClient() {
+ std::scoped_lock lock{m_mutex};
+ return m_networkClient;
+}
+
+std::optional<int64_t> InstanceImpl::GetServerTimeOffset() {
+ std::scoped_lock lock{m_mutex};
+ return m_serverTimeOffset;
+}
+
+void InstanceImpl::AddTimeSyncListener(NT_Listener listener,
+ unsigned int eventMask) {
+ std::scoped_lock lock{m_mutex};
+ eventMask &= (NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE);
+ listenerStorage.Activate(listener, eventMask);
+ if ((eventMask & (NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE)) ==
+ (NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE) &&
+ m_serverTimeOffset) {
+ listenerStorage.NotifyTimeSync({&listener, 1},
+ NT_EVENT_TIMESYNC | NT_EVENT_IMMEDIATE,
+ *m_serverTimeOffset, m_rtt2, true);
+ }
+}
+
+void InstanceImpl::Reset() {
+ std::scoped_lock lock{m_mutex};
+ m_networkServer.reset();
+ m_networkClient.reset();
+ m_servers.clear();
+ networkMode = NT_NET_MODE_NONE;
+ m_serverTimeOffset.reset();
+ m_rtt2 = 0;
+
+ listenerStorage.Reset();
+ // connectionList should have been cleared by destroying networkClient/server
+ localStorage.Reset();
}
diff --git a/ntcore/src/main/native/cpp/InstanceImpl.h b/ntcore/src/main/native/cpp/InstanceImpl.h
index 22d94f5..a7b4742 100644
--- a/ntcore/src/main/native/cpp/InstanceImpl.h
+++ b/ntcore/src/main/native/cpp/InstanceImpl.h
@@ -2,45 +2,72 @@
// 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.
-#ifndef NTCORE_INSTANCEIMPL_H_
-#define NTCORE_INSTANCEIMPL_H_
+#pragma once
#include <atomic>
#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
#include <wpi/mutex.h>
-#include "ConnectionNotifier.h"
-#include "Dispatcher.h"
-#include "DsClient.h"
-#include "EntryNotifier.h"
+#include "ConnectionList.h"
+#include "Handle.h"
+#include "ListenerStorage.h"
+#include "LocalStorage.h"
#include "Log.h"
#include "LoggerImpl.h"
-#include "RpcServer.h"
-#include "Storage.h"
+#include "NetworkClient.h"
+#include "NetworkServer.h"
namespace nt {
class InstanceImpl {
public:
explicit InstanceImpl(int inst);
- ~InstanceImpl();
// Instance repository
static InstanceImpl* GetDefault();
static InstanceImpl* Get(int inst);
+ static InstanceImpl* GetHandle(NT_Handle handle) {
+ return Get(Handle{handle}.GetInst());
+ }
+ static InstanceImpl* GetTyped(NT_Handle handle, Handle::Type type) {
+ return Get(Handle{handle}.GetTypedInst(type));
+ }
static int GetDefaultIndex();
static int Alloc();
static void Destroy(int inst);
+ void StartLocal();
+ void StopLocal();
+ void StartServer(std::string_view persistFilename,
+ std::string_view listenAddress, unsigned int port3,
+ unsigned int port4);
+ void StopServer();
+ void StartClient3(std::string_view identity);
+ void StartClient4(std::string_view identity);
+ void StopClient();
+ void SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers);
+
+ std::shared_ptr<NetworkServer> GetServer();
+ std::shared_ptr<INetworkClient> GetClient();
+
+ std::optional<int64_t> GetServerTimeOffset();
+ void AddTimeSyncListener(NT_Listener listener, unsigned int eventMask);
+
+ void Reset();
+
+ ListenerStorage listenerStorage;
LoggerImpl logger_impl;
wpi::Logger logger;
- ConnectionNotifier connection_notifier;
- EntryNotifier entry_notifier;
- RpcServer rpc_server;
- Storage storage;
- Dispatcher dispatcher;
- DsClient ds_client;
+ ConnectionList connectionList;
+ LocalStorage localStorage;
+ std::atomic<int> networkMode{NT_NET_MODE_NONE};
private:
static int AllocImpl();
@@ -49,8 +76,14 @@
static constexpr int kNumInstances = 16;
static std::atomic<InstanceImpl*> s_instances[kNumInstances];
static wpi::mutex s_mutex;
+
+ wpi::mutex m_mutex;
+ std::shared_ptr<NetworkServer> m_networkServer;
+ std::shared_ptr<INetworkClient> m_networkClient;
+ std::vector<std::pair<std::string, unsigned int>> m_servers;
+ std::optional<int64_t> m_serverTimeOffset;
+ int64_t m_rtt2 = 0;
+ int m_inst;
};
} // namespace nt
-
-#endif // NTCORE_INSTANCEIMPL_H_
diff --git a/ntcore/src/main/native/cpp/ListenerStorage.cpp b/ntcore/src/main/native/cpp/ListenerStorage.cpp
new file mode 100644
index 0000000..fdbf03b
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ListenerStorage.cpp
@@ -0,0 +1,405 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "ListenerStorage.h"
+
+#include <algorithm>
+
+#include <wpi/DenseMap.h>
+#include <wpi/SmallVector.h>
+
+#include "ntcore_c.h"
+
+using namespace nt;
+
+class ListenerStorage::Thread final : public wpi::SafeThreadEvent {
+ public:
+ explicit Thread(NT_ListenerPoller poller) : m_poller{poller} {}
+
+ void Main() final;
+
+ NT_ListenerPoller m_poller;
+ wpi::DenseMap<NT_Listener, ListenerCallback> m_callbacks;
+ wpi::Event m_waitQueueWakeup;
+ wpi::Event m_waitQueueWaiter;
+};
+
+void ListenerStorage::Thread::Main() {
+ while (m_active) {
+ WPI_Handle signaledBuf[3];
+ auto signaled = wpi::WaitForObjects(
+ {m_poller, m_stopEvent.GetHandle(), m_waitQueueWakeup.GetHandle()},
+ signaledBuf);
+ if (signaled.empty() || !m_active) {
+ return;
+ }
+ // call all the way back out to the C++ API to ensure valid handle
+ auto events = nt::ReadListenerQueue(m_poller);
+ if (!events.empty()) {
+ std::unique_lock lock{m_mutex};
+ for (auto&& event : events) {
+ auto callbackIt = m_callbacks.find(event.listener);
+ if (callbackIt != m_callbacks.end()) {
+ auto callback = callbackIt->second;
+ lock.unlock();
+ callback(event);
+ lock.lock();
+ }
+ }
+ }
+ if (std::find(signaled.begin(), signaled.end(),
+ m_waitQueueWakeup.GetHandle()) != signaled.end()) {
+ m_waitQueueWaiter.Set();
+ }
+ }
+}
+
+ListenerStorage::ListenerStorage(int inst) : m_inst{inst} {}
+
+ListenerStorage::~ListenerStorage() = default;
+
+void ListenerStorage::Activate(NT_Listener listenerHandle, unsigned int mask,
+ FinishEventFunc finishEvent) {
+ std::scoped_lock lock{m_mutex};
+ if (auto listener = m_listeners.Get(listenerHandle)) {
+ listener->sources.emplace_back(std::move(finishEvent), mask);
+ unsigned int deltaMask = mask & (~listener->eventMask);
+ listener->eventMask |= mask;
+
+ if ((deltaMask & NT_EVENT_CONNECTION) != 0) {
+ m_connListeners.Add(listener);
+ }
+ if ((deltaMask & NT_EVENT_TOPIC) != 0) {
+ m_topicListeners.Add(listener);
+ }
+ if ((deltaMask & NT_EVENT_VALUE_ALL) != 0) {
+ m_valueListeners.Add(listener);
+ }
+ // detect the higher log bits too; see LoggerImpl
+ if ((deltaMask & NT_EVENT_LOGMESSAGE) != 0 ||
+ (deltaMask & 0x1ff0000) != 0) {
+ m_logListeners.Add(listener);
+ }
+ if ((deltaMask & NT_EVENT_TIMESYNC) != 0) {
+ m_timeSyncListeners.Add(listener);
+ }
+ }
+}
+
+void ListenerStorage::Notify(std::span<const NT_Listener> handles,
+ unsigned int flags,
+ std::span<ConnectionInfo const* const> infos) {
+ if (flags == 0) {
+ return;
+ }
+ std::scoped_lock lock{m_mutex};
+
+ auto doSignal = [&](ListenerData& listener) {
+ if ((flags & listener.eventMask) != 0) {
+ for (auto&& [finishEvent, mask] : listener.sources) {
+ if ((flags & mask) != 0) {
+ for (auto&& info : infos) {
+ listener.poller->queue.emplace_back(listener.handle, flags, *info);
+ // finishEvent is never set (see ConnectionList)
+ }
+ }
+ }
+ listener.handle.Set();
+ listener.poller->handle.Set();
+ }
+ };
+
+ if (!handles.empty()) {
+ for (auto handle : handles) {
+ if (auto listener = m_listeners.Get(handle)) {
+ doSignal(*listener);
+ }
+ }
+ } else {
+ for (auto&& listener : m_connListeners) {
+ doSignal(*listener);
+ }
+ }
+}
+
+void ListenerStorage::Notify(std::span<const NT_Listener> handles,
+ unsigned int flags,
+ std::span<const TopicInfo> infos) {
+ if (flags == 0) {
+ return;
+ }
+ std::scoped_lock lock{m_mutex};
+
+ auto doSignal = [&](ListenerData& listener) {
+ if ((flags & listener.eventMask) != 0) {
+ int count = 0;
+ for (auto&& [finishEvent, mask] : listener.sources) {
+ if ((flags & mask) != 0) {
+ for (auto&& info : infos) {
+ listener.poller->queue.emplace_back(listener.handle, flags, info);
+ if (finishEvent &&
+ !finishEvent(mask, &listener.poller->queue.back())) {
+ listener.poller->queue.pop_back();
+ } else {
+ ++count;
+ }
+ }
+ }
+ }
+ if (count > 0) {
+ listener.handle.Set();
+ listener.poller->handle.Set();
+ }
+ }
+ };
+
+ if (!handles.empty()) {
+ for (auto handle : handles) {
+ if (auto listener = m_listeners.Get(handle)) {
+ doSignal(*listener);
+ }
+ }
+ } else {
+ for (auto&& listener : m_topicListeners) {
+ doSignal(*listener);
+ }
+ }
+}
+
+void ListenerStorage::Notify(std::span<const NT_Listener> handles,
+ unsigned int flags, NT_Topic topic,
+ NT_Handle subentry, const Value& value) {
+ if (flags == 0) {
+ return;
+ }
+ std::scoped_lock lock{m_mutex};
+
+ auto doSignal = [&](ListenerData& listener) {
+ if ((flags & listener.eventMask) != 0) {
+ int count = 0;
+ for (auto&& [finishEvent, mask] : listener.sources) {
+ if ((flags & mask) != 0) {
+ listener.poller->queue.emplace_back(listener.handle, flags, topic,
+ subentry, value);
+ if (finishEvent &&
+ !finishEvent(mask, &listener.poller->queue.back())) {
+ listener.poller->queue.pop_back();
+ } else {
+ ++count;
+ }
+ }
+ }
+ if (count > 0) {
+ listener.handle.Set();
+ listener.poller->handle.Set();
+ }
+ }
+ };
+
+ if (!handles.empty()) {
+ for (auto handle : handles) {
+ if (auto listener = m_listeners.Get(handle)) {
+ doSignal(*listener);
+ }
+ }
+ } else {
+ for (auto&& listener : m_valueListeners) {
+ doSignal(*listener);
+ }
+ }
+}
+
+void ListenerStorage::Notify(unsigned int flags, unsigned int level,
+ std::string_view filename, unsigned int line,
+ std::string_view message) {
+ if (flags == 0) {
+ return;
+ }
+ std::scoped_lock lock{m_mutex};
+ for (auto&& listener : m_logListeners) {
+ if ((flags & listener->eventMask) != 0) {
+ int count = 0;
+ for (auto&& [finishEvent, mask] : listener->sources) {
+ if ((flags & mask) != 0) {
+ listener->poller->queue.emplace_back(listener->handle, flags, level,
+ filename, line, message);
+ if (finishEvent &&
+ !finishEvent(mask, &listener->poller->queue.back())) {
+ listener->poller->queue.pop_back();
+ } else {
+ ++count;
+ }
+ }
+ }
+ if (count > 0) {
+ listener->handle.Set();
+ listener->poller->handle.Set();
+ }
+ }
+ }
+}
+
+void ListenerStorage::NotifyTimeSync(std::span<const NT_Listener> handles,
+ unsigned int flags,
+ int64_t serverTimeOffset, int64_t rtt2,
+ bool valid) {
+ if (flags == 0) {
+ return;
+ }
+ std::scoped_lock lock{m_mutex};
+
+ auto doSignal = [&](ListenerData& listener) {
+ if ((flags & listener.eventMask) != 0) {
+ for (auto&& [finishEvent, mask] : listener.sources) {
+ if ((flags & mask) != 0) {
+ listener.poller->queue.emplace_back(listener.handle, flags,
+ serverTimeOffset, rtt2, valid);
+ // finishEvent is never set (see InstanceImpl)
+ }
+ }
+ listener.handle.Set();
+ listener.poller->handle.Set();
+ }
+ };
+
+ if (!handles.empty()) {
+ for (auto handle : handles) {
+ if (auto listener = m_listeners.Get(handle)) {
+ doSignal(*listener);
+ }
+ }
+ } else {
+ for (auto&& listener : m_timeSyncListeners) {
+ doSignal(*listener);
+ }
+ }
+}
+
+NT_Listener ListenerStorage::AddListener(ListenerCallback callback) {
+ std::scoped_lock lock{m_mutex};
+ if (!m_thread) {
+ m_thread.Start(m_pollers.Add(m_inst)->handle);
+ }
+ if (auto thr = m_thread.GetThread()) {
+ auto listener = DoAddListener(thr->m_poller);
+ if (listener) {
+ thr->m_callbacks.try_emplace(listener, std::move(callback));
+ }
+ return listener;
+ } else {
+ return {};
+ }
+}
+
+NT_Listener ListenerStorage::AddListener(NT_ListenerPoller pollerHandle) {
+ std::scoped_lock lock{m_mutex};
+ return DoAddListener(pollerHandle);
+}
+
+NT_Listener ListenerStorage::DoAddListener(NT_ListenerPoller pollerHandle) {
+ if (auto poller = m_pollers.Get(pollerHandle)) {
+ return m_listeners.Add(m_inst, poller)->handle;
+ } else {
+ return {};
+ }
+}
+
+NT_ListenerPoller ListenerStorage::CreateListenerPoller() {
+ std::scoped_lock lock{m_mutex};
+ return m_pollers.Add(m_inst)->handle;
+}
+
+std::vector<std::pair<NT_Listener, unsigned int>>
+ListenerStorage::DestroyListenerPoller(NT_ListenerPoller pollerHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto poller = m_pollers.Remove(pollerHandle)) {
+ // ensure all listeners that use this poller are removed
+ wpi::SmallVector<NT_Listener, 16> toRemove;
+ for (auto&& listener : m_listeners) {
+ if (listener->poller == poller.get()) {
+ toRemove.emplace_back(listener->handle);
+ }
+ }
+ return DoRemoveListeners(toRemove);
+ } else {
+ return {};
+ }
+}
+
+std::vector<Event> ListenerStorage::ReadListenerQueue(
+ NT_ListenerPoller pollerHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto poller = m_pollers.Get(pollerHandle)) {
+ std::vector<Event> rv;
+ rv.swap(poller->queue);
+ return rv;
+ } else {
+ return {};
+ }
+}
+
+std::vector<std::pair<NT_Listener, unsigned int>>
+ListenerStorage::RemoveListener(NT_Listener listenerHandle) {
+ std::scoped_lock lock{m_mutex};
+ return DoRemoveListeners({&listenerHandle, 1});
+}
+
+bool ListenerStorage::WaitForListenerQueue(double timeout) {
+ WPI_EventHandle h;
+ {
+ std::scoped_lock lock{m_mutex};
+ if (auto thr = m_thread.GetThread()) {
+ h = thr->m_waitQueueWaiter.GetHandle();
+ thr->m_waitQueueWakeup.Set();
+ } else {
+ return false;
+ }
+ }
+ bool timedOut;
+ wpi::WaitForObject(h, timeout, &timedOut);
+ return !timedOut;
+}
+
+void ListenerStorage::Reset() {
+ std::scoped_lock lock{m_mutex};
+ m_pollers.clear();
+ m_listeners.clear();
+ m_connListeners.clear();
+ m_topicListeners.clear();
+ m_valueListeners.clear();
+ m_logListeners.clear();
+ if (m_thread) {
+ m_thread.Stop();
+ }
+}
+
+std::vector<std::pair<NT_Listener, unsigned int>>
+ListenerStorage::DoRemoveListeners(std::span<const NT_Listener> handles) {
+ std::vector<std::pair<NT_Listener, unsigned int>> rv;
+ auto thr = m_thread.GetThread();
+ for (auto handle : handles) {
+ if (auto listener = m_listeners.Remove(handle)) {
+ rv.emplace_back(handle, listener->eventMask);
+ if (thr) {
+ if (thr->m_poller == listener->poller->handle) {
+ thr->m_callbacks.erase(handle);
+ }
+ }
+ if ((listener->eventMask & NT_EVENT_CONNECTION) != 0) {
+ m_connListeners.Remove(listener.get());
+ }
+ if ((listener->eventMask & NT_EVENT_TOPIC) != 0) {
+ m_topicListeners.Remove(listener.get());
+ }
+ if ((listener->eventMask & NT_EVENT_VALUE_ALL) != 0) {
+ m_valueListeners.Remove(listener.get());
+ }
+ if ((listener->eventMask & NT_EVENT_LOGMESSAGE) != 0 ||
+ (listener->eventMask & 0x1ff0000) != 0) {
+ m_logListeners.Remove(listener.get());
+ }
+ }
+ }
+ return rv;
+}
diff --git a/ntcore/src/main/native/cpp/ListenerStorage.h b/ntcore/src/main/native/cpp/ListenerStorage.h
new file mode 100644
index 0000000..b291a12
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ListenerStorage.h
@@ -0,0 +1,116 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <span>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <wpi/SafeThread.h>
+#include <wpi/SmallVector.h>
+#include <wpi/Synchronization.h>
+#include <wpi/mutex.h>
+
+#include "Handle.h"
+#include "HandleMap.h"
+#include "IListenerStorage.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class ListenerStorage final : public IListenerStorage {
+ public:
+ explicit ListenerStorage(int inst);
+ ListenerStorage(const ListenerStorage&) = delete;
+ ListenerStorage& operator=(const ListenerStorage&) = delete;
+ ~ListenerStorage() final;
+
+ // IListenerStorage interface
+ void Activate(NT_Listener listenerHandle, unsigned int mask,
+ FinishEventFunc finishEvent = {}) final;
+ void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ std::span<ConnectionInfo const* const> infos) final;
+ void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ std::span<const TopicInfo> infos) final;
+ void Notify(std::span<const NT_Listener> handles, unsigned int flags,
+ NT_Topic topic, NT_Handle subentry, const Value& value) final;
+ void Notify(unsigned int flags, unsigned int level, std::string_view filename,
+ unsigned int line, std::string_view message) final;
+ void NotifyTimeSync(std::span<const NT_Listener> handles, unsigned int flags,
+ int64_t serverTimeOffset, int64_t rtt2, bool valid) final;
+
+ // user-facing functions
+ NT_Listener AddListener(ListenerCallback callback);
+ NT_Listener AddListener(NT_ListenerPoller pollerHandle);
+ NT_ListenerPoller CreateListenerPoller();
+
+ // returns listener handle and mask for each listener that was destroyed
+ [[nodiscard]] std::vector<std::pair<NT_Listener, unsigned int>>
+ DestroyListenerPoller(NT_ListenerPoller pollerHandle);
+
+ std::vector<Event> ReadListenerQueue(NT_ListenerPoller pollerHandle);
+
+ // returns listener handle and mask for each listener that was destroyed
+ [[nodiscard]] std::vector<std::pair<NT_Listener, unsigned int>>
+ RemoveListener(NT_Listener listenerHandle);
+
+ bool WaitForListenerQueue(double timeout);
+
+ void Reset();
+
+ private:
+ // these assume the mutex is already held
+ NT_Listener DoAddListener(NT_ListenerPoller pollerHandle);
+ std::vector<std::pair<NT_Listener, unsigned int>> DoRemoveListeners(
+ std::span<const NT_Listener> handles);
+
+ int m_inst;
+ mutable wpi::mutex m_mutex;
+
+ struct PollerData {
+ static constexpr auto kType = Handle::kListenerPoller;
+
+ explicit PollerData(NT_ListenerPoller handle) : handle{handle} {}
+
+ wpi::SignalObject<NT_ListenerPoller> handle;
+ std::vector<Event> queue;
+ };
+ HandleMap<PollerData, 8> m_pollers;
+
+ struct ListenerData {
+ static constexpr auto kType = Handle::kListener;
+
+ ListenerData(NT_Listener handle, PollerData* poller)
+ : handle{handle}, poller{poller} {}
+
+ wpi::SignalObject<NT_Listener> handle;
+ PollerData* poller;
+ wpi::SmallVector<std::pair<FinishEventFunc, unsigned int>, 2> sources;
+ unsigned int eventMask{0};
+ };
+ HandleMap<ListenerData, 8> m_listeners;
+
+ // Utility wrapper for making a set-like vector
+ template <typename T>
+ class VectorSet : public std::vector<T> {
+ public:
+ void Add(T value) { this->push_back(value); }
+ void Remove(T value) { std::erase(*this, value); }
+ };
+
+ VectorSet<ListenerData*> m_connListeners;
+ VectorSet<ListenerData*> m_topicListeners;
+ VectorSet<ListenerData*> m_valueListeners;
+ VectorSet<ListenerData*> m_logListeners;
+ VectorSet<ListenerData*> m_timeSyncListeners;
+
+ class Thread;
+ wpi::SafeThreadOwner<Thread> m_thread;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/LocalStorage.cpp b/ntcore/src/main/native/cpp/LocalStorage.cpp
new file mode 100644
index 0000000..db8e065
--- /dev/null
+++ b/ntcore/src/main/native/cpp/LocalStorage.cpp
@@ -0,0 +1,2264 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "LocalStorage.h"
+
+#include <algorithm>
+
+#include <wpi/DataLog.h>
+#include <wpi/StringExtras.h>
+#include <wpi/StringMap.h>
+#include <wpi/Synchronization.h>
+#include <wpi/UidVector.h>
+#include <wpi/circular_buffer.h>
+#include <wpi/json.h>
+
+#include "Handle.h"
+#include "HandleMap.h"
+#include "IListenerStorage.h"
+#include "Log.h"
+#include "PubSubOptions.h"
+#include "Types_internal.h"
+#include "Value_internal.h"
+#include "networktables/NetworkTableValue.h"
+#include "ntcore_c.h"
+
+using namespace nt;
+
+// maximum number of local publishers / subscribers to any given topic
+static constexpr size_t kMaxPublishers = 512;
+static constexpr size_t kMaxSubscribers = 512;
+static constexpr size_t kMaxMultiSubscribers = 512;
+static constexpr size_t kMaxListeners = 512;
+
+namespace {
+
+static constexpr bool IsSpecial(std::string_view name) {
+ return name.empty() ? false : name.front() == '$';
+}
+
+static constexpr bool PrefixMatch(std::string_view name,
+ std::string_view prefix, bool special) {
+ return (!special || !prefix.empty()) && wpi::starts_with(name, prefix);
+}
+
+// Utility wrapper for making a set-like vector
+template <typename T>
+class VectorSet : public std::vector<T> {
+ public:
+ void Add(T value) { this->push_back(value); }
+ void Remove(T value) { std::erase(*this, value); }
+};
+
+struct EntryData;
+struct PublisherData;
+struct SubscriberData;
+struct MultiSubscriberData;
+
+struct DataLoggerEntry {
+ DataLoggerEntry(wpi::log::DataLog& log, int entry, NT_DataLogger logger)
+ : log{&log}, entry{entry}, logger{logger} {}
+
+ static std::string MakeMetadata(std::string_view properties) {
+ return fmt::format("{{\"properties\":{},\"source\":\"NT\"}}", properties);
+ }
+
+ void Append(const Value& v);
+
+ wpi::log::DataLog* log;
+ int entry;
+ NT_DataLogger logger;
+};
+
+struct TopicData {
+ static constexpr auto kType = Handle::kTopic;
+
+ TopicData(NT_Topic handle, std::string_view name)
+ : handle{handle}, name{name}, special{IsSpecial(name)} {}
+
+ bool Exists() const { return onNetwork || !localPublishers.empty(); }
+
+ TopicInfo GetTopicInfo() const;
+
+ // invariants
+ wpi::SignalObject<NT_Topic> handle;
+ std::string name;
+ bool special;
+
+ Value lastValue; // also stores timestamp
+ Value lastValueNetwork;
+ NT_Type type{NT_UNASSIGNED};
+ std::string typeStr;
+ unsigned int flags{0}; // for NT3 APIs
+ std::string propertiesStr{"{}"}; // cached string for GetTopicInfo() et al
+ wpi::json properties = wpi::json::object();
+ NT_Entry entry{0}; // cached entry for GetEntry()
+
+ bool onNetwork{false}; // true if there are any remote publishers
+
+ wpi::SmallVector<DataLoggerEntry, 1> datalogs;
+ NT_Type datalogType{NT_UNASSIGNED};
+
+ VectorSet<PublisherData*> localPublishers;
+ VectorSet<SubscriberData*> localSubscribers;
+ VectorSet<MultiSubscriberData*> multiSubscribers;
+ VectorSet<EntryData*> entries;
+ VectorSet<NT_Listener> listeners;
+};
+
+struct PubSubConfig : public PubSubOptionsImpl {
+ PubSubConfig() = default;
+ PubSubConfig(NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options)
+ : PubSubOptionsImpl{options}, type{type}, typeStr{typeStr} {
+ prefixMatch = false;
+ }
+
+ NT_Type type{NT_UNASSIGNED};
+ std::string typeStr;
+};
+
+struct PublisherData {
+ static constexpr auto kType = Handle::kPublisher;
+
+ PublisherData(NT_Publisher handle, TopicData* topic, PubSubConfig config)
+ : handle{handle}, topic{topic}, config{std::move(config)} {}
+
+ void UpdateActive();
+
+ // invariants
+ wpi::SignalObject<NT_Publisher> handle;
+ TopicData* topic;
+ PubSubConfig config;
+
+ // whether or not the publisher should actually publish values
+ bool active{false};
+};
+
+struct SubscriberData {
+ static constexpr auto kType = Handle::kSubscriber;
+
+ SubscriberData(NT_Subscriber handle, TopicData* topic, PubSubConfig config)
+ : handle{handle},
+ topic{topic},
+ config{std::move(config)},
+ pollStorage{config.pollStorage} {}
+
+ void UpdateActive();
+
+ // invariants
+ wpi::SignalObject<NT_Subscriber> handle;
+ TopicData* topic;
+ PubSubConfig config;
+
+ // whether or not the subscriber should actually receive values
+ bool active{false};
+
+ // polling storage
+ wpi::circular_buffer<Value> pollStorage;
+
+ // value listeners
+ VectorSet<NT_Listener> valueListeners;
+};
+
+struct EntryData {
+ static constexpr auto kType = Handle::kEntry;
+
+ EntryData(NT_Entry handle, SubscriberData* subscriber)
+ : handle{handle}, topic{subscriber->topic}, subscriber{subscriber} {}
+
+ // invariants
+ wpi::SignalObject<NT_Entry> handle;
+ TopicData* topic;
+ SubscriberData* subscriber;
+
+ // the publisher (created on demand)
+ PublisherData* publisher{nullptr};
+};
+
+struct MultiSubscriberData {
+ static constexpr auto kType = Handle::kMultiSubscriber;
+
+ MultiSubscriberData(NT_MultiSubscriber handle,
+ std::span<const std::string_view> prefixes,
+ const PubSubOptionsImpl& options)
+ : handle{handle}, options{options} {
+ this->options.prefixMatch = true;
+ this->prefixes.reserve(prefixes.size());
+ for (auto&& prefix : prefixes) {
+ this->prefixes.emplace_back(prefix);
+ }
+ }
+
+ bool Matches(std::string_view name, bool special);
+
+ // invariants
+ wpi::SignalObject<NT_MultiSubscriber> handle;
+ std::vector<std::string> prefixes;
+ PubSubOptionsImpl options;
+
+ // value listeners
+ VectorSet<NT_Listener> valueListeners;
+};
+
+bool MultiSubscriberData::Matches(std::string_view name, bool special) {
+ for (auto&& prefix : prefixes) {
+ if (PrefixMatch(name, prefix, special)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+struct ListenerData {
+ ListenerData(NT_Listener handle, SubscriberData* subscriber,
+ unsigned int eventMask, bool subscriberOwned)
+ : handle{handle},
+ eventMask{eventMask},
+ subscriber{subscriber},
+ subscriberOwned{subscriberOwned} {}
+ ListenerData(NT_Listener handle, MultiSubscriberData* subscriber,
+ unsigned int eventMask, bool subscriberOwned)
+ : handle{handle},
+ eventMask{eventMask},
+ multiSubscriber{subscriber},
+ subscriberOwned{subscriberOwned} {}
+
+ NT_Listener handle;
+ unsigned int eventMask;
+ SubscriberData* subscriber{nullptr};
+ MultiSubscriberData* multiSubscriber{nullptr};
+ bool subscriberOwned;
+};
+
+struct DataLoggerData {
+ static constexpr auto kType = Handle::kDataLogger;
+
+ DataLoggerData(NT_DataLogger handle, wpi::log::DataLog& log,
+ std::string_view prefix, std::string_view logPrefix)
+ : handle{handle}, log{log}, prefix{prefix}, logPrefix{logPrefix} {}
+
+ int Start(TopicData* topic, int64_t time) {
+ return log.Start(fmt::format("{}{}", logPrefix,
+ wpi::drop_front(topic->name, prefix.size())),
+ topic->typeStr,
+ DataLoggerEntry::MakeMetadata(topic->propertiesStr), time);
+ }
+
+ NT_DataLogger handle;
+ wpi::log::DataLog& log;
+ std::string prefix;
+ std::string logPrefix;
+};
+
+struct LSImpl {
+ LSImpl(int inst, IListenerStorage& listenerStorage, wpi::Logger& logger)
+ : m_inst{inst}, m_listenerStorage{listenerStorage}, m_logger{logger} {}
+
+ int m_inst;
+ IListenerStorage& m_listenerStorage;
+ wpi::Logger& m_logger;
+ net::NetworkInterface* m_network{nullptr};
+
+ // handle mappings
+ HandleMap<TopicData, 16> m_topics;
+ HandleMap<PublisherData, 16> m_publishers;
+ HandleMap<SubscriberData, 16> m_subscribers;
+ HandleMap<EntryData, 16> m_entries;
+ HandleMap<MultiSubscriberData, 16> m_multiSubscribers;
+ HandleMap<DataLoggerData, 16> m_dataloggers;
+
+ // name mappings
+ wpi::StringMap<TopicData*> m_nameTopics;
+
+ // listeners
+ wpi::DenseMap<NT_Listener, std::unique_ptr<ListenerData>> m_listeners;
+
+ // string-based listeners
+ VectorSet<ListenerData*> m_topicPrefixListeners;
+
+ // topic functions
+ void NotifyTopic(TopicData* topic, unsigned int eventFlags);
+
+ void CheckReset(TopicData* topic);
+
+ bool SetValue(TopicData* topic, const Value& value, unsigned int eventFlags,
+ bool isDuplicate, const PublisherData* publisher);
+ void NotifyValue(TopicData* topic, unsigned int eventFlags, bool isDuplicate,
+ const PublisherData* publisher);
+
+ void SetFlags(TopicData* topic, unsigned int flags);
+ void SetPersistent(TopicData* topic, bool value);
+ void SetRetained(TopicData* topic, bool value);
+ void SetProperties(TopicData* topic, const wpi::json& update,
+ bool sendNetwork);
+ void PropertiesUpdated(TopicData* topic, const wpi::json& update,
+ unsigned int eventFlags, bool sendNetwork,
+ bool updateFlags = true);
+
+ void RefreshPubSubActive(TopicData* topic, bool warnOnSubMismatch);
+
+ void NetworkAnnounce(TopicData* topic, std::string_view typeStr,
+ const wpi::json& properties, NT_Publisher pubHandle);
+ void RemoveNetworkPublisher(TopicData* topic);
+ void NetworkPropertiesUpdate(TopicData* topic, const wpi::json& update,
+ bool ack);
+
+ PublisherData* AddLocalPublisher(TopicData* topic,
+ const wpi::json& properties,
+ const PubSubConfig& options);
+ std::unique_ptr<PublisherData> RemoveLocalPublisher(NT_Publisher pubHandle);
+
+ SubscriberData* AddLocalSubscriber(TopicData* topic,
+ const PubSubConfig& options);
+ std::unique_ptr<SubscriberData> RemoveLocalSubscriber(
+ NT_Subscriber subHandle);
+
+ EntryData* AddEntry(SubscriberData* subscriber);
+ std::unique_ptr<EntryData> RemoveEntry(NT_Entry entryHandle);
+
+ MultiSubscriberData* AddMultiSubscriber(
+ std::span<const std::string_view> prefixes, const PubSubOptions& options);
+ std::unique_ptr<MultiSubscriberData> RemoveMultiSubscriber(
+ NT_MultiSubscriber subHandle);
+
+ void AddListenerImpl(NT_Listener listenerHandle, TopicData* topic,
+ unsigned int eventMask);
+ void AddListenerImpl(NT_Listener listenerHandle, SubscriberData* subscriber,
+ unsigned int eventMask, NT_Handle subentryHandle,
+ bool subscriberOwned);
+ void AddListenerImpl(NT_Listener listenerHandle,
+ MultiSubscriberData* subscriber, unsigned int eventMask,
+ bool subscriberOwned);
+ void AddListenerImpl(NT_Listener listenerHandle,
+ std::span<const std::string_view> prefixes,
+ unsigned int eventMask);
+
+ void AddListener(NT_Listener listenerHandle,
+ std::span<const std::string_view> prefixes,
+ unsigned int mask);
+ void AddListener(NT_Listener listenerHandle, NT_Handle handle,
+ unsigned int mask);
+ void RemoveListener(NT_Listener listenerHandle, unsigned int mask);
+
+ TopicData* GetOrCreateTopic(std::string_view name);
+ TopicData* GetTopic(NT_Handle handle);
+ SubscriberData* GetSubEntry(NT_Handle subentryHandle);
+ PublisherData* PublishEntry(EntryData* entry, NT_Type type);
+ Value* GetSubEntryValue(NT_Handle subentryHandle);
+
+ bool PublishLocalValue(PublisherData* publisher, const Value& value,
+ bool force = false);
+
+ bool SetEntryValue(NT_Handle pubentryHandle, const Value& value);
+ bool SetDefaultEntryValue(NT_Handle pubsubentryHandle, const Value& value);
+
+ void RemoveSubEntry(NT_Handle subentryHandle);
+};
+
+} // namespace
+
+void DataLoggerEntry::Append(const Value& v) {
+ auto time = v.time();
+ switch (v.type()) {
+ case NT_BOOLEAN:
+ log->AppendBoolean(entry, v.GetBoolean(), time);
+ break;
+ case NT_INTEGER:
+ log->AppendInteger(entry, v.GetInteger(), time);
+ break;
+ case NT_FLOAT:
+ log->AppendFloat(entry, v.GetFloat(), time);
+ break;
+ case NT_DOUBLE:
+ log->AppendDouble(entry, v.GetDouble(), time);
+ break;
+ case NT_STRING:
+ log->AppendString(entry, v.GetString(), time);
+ break;
+ case NT_RAW: {
+ auto val = v.GetRaw();
+ log->AppendRaw(entry,
+ {reinterpret_cast<const uint8_t*>(val.data()), val.size()},
+ time);
+ break;
+ }
+ case NT_BOOLEAN_ARRAY:
+ log->AppendBooleanArray(entry, v.GetBooleanArray(), time);
+ break;
+ case NT_INTEGER_ARRAY:
+ log->AppendIntegerArray(entry, v.GetIntegerArray(), time);
+ break;
+ case NT_FLOAT_ARRAY:
+ log->AppendFloatArray(entry, v.GetFloatArray(), time);
+ break;
+ case NT_DOUBLE_ARRAY:
+ log->AppendDoubleArray(entry, v.GetDoubleArray(), time);
+ break;
+ case NT_STRING_ARRAY:
+ log->AppendStringArray(entry, v.GetStringArray(), time);
+ break;
+ default:
+ break;
+ }
+}
+
+TopicInfo TopicData::GetTopicInfo() const {
+ TopicInfo info;
+ info.topic = handle;
+ info.name = name;
+ info.type = type;
+ info.type_str = typeStr;
+ info.properties = propertiesStr;
+ return info;
+}
+
+void PublisherData::UpdateActive() {
+ active = config.type == topic->type && config.typeStr == topic->typeStr;
+}
+
+void SubscriberData::UpdateActive() {
+ // for subscribers, unassigned is a wildcard
+ // also allow numerically compatible subscribers
+ active = config.type == NT_UNASSIGNED ||
+ (config.type == topic->type && config.typeStr == topic->typeStr) ||
+ IsNumericCompatible(config.type, topic->type);
+}
+
+void LSImpl::NotifyTopic(TopicData* topic, unsigned int eventFlags) {
+ DEBUG4("NotifyTopic({}, {})", topic->name, eventFlags);
+ auto topicInfo = topic->GetTopicInfo();
+ if (!topic->listeners.empty()) {
+ m_listenerStorage.Notify(topic->listeners, eventFlags, topicInfo);
+ }
+
+ wpi::SmallVector<NT_Listener, 32> listeners;
+ for (auto listener : m_topicPrefixListeners) {
+ if (listener->multiSubscriber &&
+ listener->multiSubscriber->Matches(topic->name, topic->special)) {
+ listeners.emplace_back(listener->handle);
+ }
+ }
+ if (!listeners.empty()) {
+ m_listenerStorage.Notify(listeners, eventFlags, topicInfo);
+ }
+
+ if ((eventFlags & (NT_EVENT_PUBLISH | NT_EVENT_UNPUBLISH)) != 0) {
+ if (!m_dataloggers.empty()) {
+ auto now = Now();
+ for (auto&& datalogger : m_dataloggers) {
+ if (wpi::starts_with(topic->name, datalogger->prefix)) {
+ auto it = std::find_if(topic->datalogs.begin(), topic->datalogs.end(),
+ [&](const auto& elem) {
+ return elem.logger == datalogger->handle;
+ });
+ if ((eventFlags & NT_EVENT_PUBLISH) != 0 &&
+ it == topic->datalogs.end()) {
+ topic->datalogs.emplace_back(datalogger->log,
+ datalogger->Start(topic, now),
+ datalogger->handle);
+ topic->datalogType = topic->type;
+ } else if ((eventFlags & NT_EVENT_UNPUBLISH) != 0 &&
+ it != topic->datalogs.end()) {
+ it->log->Finish(it->entry, now);
+ topic->datalogType = NT_UNASSIGNED;
+ topic->datalogs.erase(it);
+ }
+ }
+ }
+ }
+ } else if ((eventFlags & NT_EVENT_PROPERTIES) != 0) {
+ if (!topic->datalogs.empty()) {
+ auto metadata = DataLoggerEntry::MakeMetadata(topic->propertiesStr);
+ for (auto&& datalog : topic->datalogs) {
+ datalog.log->SetMetadata(datalog.entry, metadata);
+ }
+ }
+ }
+}
+
+void LSImpl::CheckReset(TopicData* topic) {
+ if (topic->Exists()) {
+ return;
+ }
+ topic->lastValue = {};
+ topic->lastValueNetwork = {};
+ topic->type = NT_UNASSIGNED;
+ topic->typeStr.clear();
+ topic->flags = 0;
+ topic->properties = wpi::json::object();
+ topic->propertiesStr = "{}";
+}
+
+bool LSImpl::SetValue(TopicData* topic, const Value& value,
+ unsigned int eventFlags, bool isDuplicate,
+ const PublisherData* publisher) {
+ DEBUG4("SetValue({}, {}, {}, {})", topic->name, value.time(), eventFlags,
+ isDuplicate);
+ if (topic->type != NT_UNASSIGNED && topic->type != value.type()) {
+ return false;
+ }
+ if (!topic->lastValue || value.time() >= topic->lastValue.time()) {
+ // TODO: notify option even if older value
+ topic->type = value.type();
+ topic->lastValue = value;
+ NotifyValue(topic, eventFlags, isDuplicate, publisher);
+ }
+ if (!isDuplicate && topic->datalogType == value.type()) {
+ for (auto&& datalog : topic->datalogs) {
+ datalog.Append(value);
+ }
+ }
+ return true;
+}
+
+void LSImpl::NotifyValue(TopicData* topic, unsigned int eventFlags,
+ bool isDuplicate, const PublisherData* publisher) {
+ bool isNetwork = (eventFlags & NT_EVENT_VALUE_REMOTE) != 0;
+ for (auto&& subscriber : topic->localSubscribers) {
+ if (subscriber->active &&
+ (subscriber->config.keepDuplicates || !isDuplicate) &&
+ ((isNetwork && !subscriber->config.disableRemote) ||
+ (!isNetwork && !subscriber->config.disableLocal)) &&
+ (!publisher || (publisher && (subscriber->config.excludePublisher !=
+ publisher->handle)))) {
+ subscriber->pollStorage.emplace_back(topic->lastValue);
+ subscriber->handle.Set();
+ if (!subscriber->valueListeners.empty()) {
+ m_listenerStorage.Notify(subscriber->valueListeners, eventFlags,
+ topic->handle, 0, topic->lastValue);
+ }
+ }
+ }
+
+ for (auto&& subscriber : topic->multiSubscribers) {
+ if (subscriber->options.keepDuplicates || !isDuplicate) {
+ subscriber->handle.Set();
+ if (!subscriber->valueListeners.empty()) {
+ m_listenerStorage.Notify(subscriber->valueListeners, eventFlags,
+ topic->handle, 0, topic->lastValue);
+ }
+ }
+ }
+}
+
+void LSImpl::SetFlags(TopicData* topic, unsigned int flags) {
+ wpi::json update = wpi::json::object();
+ if ((flags & NT_PERSISTENT) != 0) {
+ topic->properties["persistent"] = true;
+ update["persistent"] = true;
+ } else {
+ topic->properties.erase("persistent");
+ update["persistent"] = wpi::json();
+ }
+ if ((flags & NT_RETAINED) != 0) {
+ topic->properties["retained"] = true;
+ update["retained"] = true;
+ } else {
+ topic->properties.erase("retained");
+ update["retained"] = wpi::json();
+ }
+ topic->flags = flags;
+ if (!update.empty()) {
+ PropertiesUpdated(topic, update, NT_EVENT_NONE, true, false);
+ }
+}
+
+void LSImpl::SetPersistent(TopicData* topic, bool value) {
+ wpi::json update = wpi::json::object();
+ if (value) {
+ topic->flags |= NT_PERSISTENT;
+ topic->properties["persistent"] = true;
+ update["persistent"] = true;
+ } else {
+ topic->flags &= ~NT_PERSISTENT;
+ topic->properties.erase("persistent");
+ update["persistent"] = wpi::json();
+ }
+ PropertiesUpdated(topic, update, NT_EVENT_NONE, true, false);
+}
+
+void LSImpl::SetRetained(TopicData* topic, bool value) {
+ wpi::json update = wpi::json::object();
+ if (value) {
+ topic->flags |= NT_RETAINED;
+ topic->properties["retained"] = true;
+ update["retained"] = true;
+ } else {
+ topic->flags &= ~NT_RETAINED;
+ topic->properties.erase("retained");
+ update["retained"] = wpi::json();
+ }
+ PropertiesUpdated(topic, update, NT_EVENT_NONE, true, false);
+}
+
+void LSImpl::SetProperties(TopicData* topic, const wpi::json& update,
+ bool sendNetwork) {
+ if (!update.is_object()) {
+ return;
+ }
+ DEBUG4("SetProperties({},{})", topic->name, sendNetwork);
+ for (auto&& change : update.items()) {
+ if (change.value().is_null()) {
+ topic->properties.erase(change.key());
+ } else {
+ topic->properties[change.key()] = change.value();
+ }
+ }
+ PropertiesUpdated(topic, update, NT_EVENT_NONE, sendNetwork);
+}
+
+void LSImpl::PropertiesUpdated(TopicData* topic, const wpi::json& update,
+ unsigned int eventFlags, bool sendNetwork,
+ bool updateFlags) {
+ DEBUG4("PropertiesUpdated({}, {}, {}, {}, {})", topic->name, update.dump(),
+ eventFlags, sendNetwork, updateFlags);
+ if (updateFlags) {
+ // set flags from properties
+ auto it = topic->properties.find("persistent");
+ if (it != topic->properties.end()) {
+ if (auto val = it->get_ptr<bool*>()) {
+ if (*val) {
+ topic->flags |= NT_PERSISTENT;
+ } else {
+ topic->flags &= ~NT_PERSISTENT;
+ }
+ }
+ }
+ it = topic->properties.find("retained");
+ if (it != topic->properties.end()) {
+ if (auto val = it->get_ptr<bool*>()) {
+ if (*val) {
+ topic->flags |= NT_RETAINED;
+ } else {
+ topic->flags &= ~NT_RETAINED;
+ }
+ }
+ }
+ }
+
+ topic->propertiesStr = topic->properties.dump();
+ NotifyTopic(topic, eventFlags | NT_EVENT_PROPERTIES);
+ // check local flag so we don't echo back received properties changes
+ if (m_network && sendNetwork) {
+ m_network->SetProperties(topic->handle, topic->name, update);
+ }
+}
+
+void LSImpl::RefreshPubSubActive(TopicData* topic, bool warnOnSubMismatch) {
+ for (auto&& publisher : topic->localPublishers) {
+ publisher->UpdateActive();
+ }
+ for (auto&& subscriber : topic->localSubscribers) {
+ subscriber->UpdateActive();
+ if (warnOnSubMismatch && topic->Exists() && !subscriber->active) {
+ // warn on type mismatch
+ INFO(
+ "local subscribe to '{}' disabled due to type mismatch (wanted '{}', "
+ "published as '{}')",
+ topic->name, subscriber->config.typeStr, topic->typeStr);
+ }
+ }
+}
+
+void LSImpl::NetworkAnnounce(TopicData* topic, std::string_view typeStr,
+ const wpi::json& properties,
+ NT_Publisher pubHandle) {
+ DEBUG4("LS NetworkAnnounce({}, {}, {}, {})", topic->name, typeStr,
+ properties.dump(), pubHandle);
+ if (pubHandle != 0) {
+ return; // ack of our publish; ignore
+ }
+
+ unsigned int event = NT_EVENT_NONE;
+ // fresh non-local publish; the network publish always sets the type even
+ // if it was locally published, but output a diagnostic for this case
+ bool didExist = topic->Exists();
+ topic->onNetwork = true;
+ NT_Type type = StringToType(typeStr);
+ if (topic->type != type || topic->typeStr != typeStr) {
+ if (didExist) {
+ INFO(
+ "network announce of '{}' overriding local publish (was '{}', now "
+ "'{}')",
+ topic->name, topic->typeStr, typeStr);
+ }
+ topic->type = type;
+ topic->typeStr = typeStr;
+ RefreshPubSubActive(topic, true);
+ }
+ if (!didExist) {
+ event |= NT_EVENT_PUBLISH;
+ }
+
+ // may be properties update, but need to compare to see if it actually
+ // changed to determine whether to update string / send event
+ wpi::json update = wpi::json::object();
+ // added/changed
+ for (auto&& prop : properties.items()) {
+ auto it = topic->properties.find(prop.key());
+ if (it == topic->properties.end() || *it != prop.value()) {
+ update[prop.key()] = prop.value();
+ }
+ }
+ // removed
+ for (auto&& prop : topic->properties.items()) {
+ if (properties.find(prop.key()) == properties.end()) {
+ update[prop.key()] = wpi::json();
+ }
+ }
+ if (!update.empty()) {
+ topic->properties = properties;
+ PropertiesUpdated(topic, update, event, false);
+ } else if (event != NT_EVENT_NONE) {
+ NotifyTopic(topic, event);
+ }
+}
+
+void LSImpl::RemoveNetworkPublisher(TopicData* topic) {
+ DEBUG4("LS RemoveNetworkPublisher({}, {})", topic->handle, topic->name);
+ // this acts as an unpublish
+ bool didExist = topic->Exists();
+ topic->onNetwork = false;
+ if (didExist && !topic->Exists()) {
+ DEBUG4("Unpublished {}", topic->name);
+ CheckReset(topic);
+ NotifyTopic(topic, NT_EVENT_UNPUBLISH);
+ }
+
+ if (!topic->localPublishers.empty()) {
+ // some other publisher still exists; if it has a different type, refresh
+ // and publish it over the network
+ auto& nextPub = topic->localPublishers.front();
+ if (nextPub->config.type != topic->type ||
+ nextPub->config.typeStr != topic->typeStr) {
+ topic->type = nextPub->config.type;
+ topic->typeStr = nextPub->config.typeStr;
+ RefreshPubSubActive(topic, false);
+ // this may result in a duplicate publish warning on the server side,
+ // but send one anyway in this case just to be sure
+ if (nextPub->active && m_network) {
+ m_network->Publish(nextPub->handle, topic->handle, topic->name,
+ topic->typeStr, topic->properties, nextPub->config);
+ }
+ }
+ }
+}
+
+void LSImpl::NetworkPropertiesUpdate(TopicData* topic, const wpi::json& update,
+ bool ack) {
+ DEBUG4("NetworkPropertiesUpdate({},{})", topic->name, ack);
+ if (ack) {
+ return; // ignore acks
+ }
+ SetProperties(topic, update, false);
+}
+
+PublisherData* LSImpl::AddLocalPublisher(TopicData* topic,
+ const wpi::json& properties,
+ const PubSubConfig& config) {
+ bool didExist = topic->Exists();
+ auto publisher = m_publishers.Add(m_inst, topic, config);
+ topic->localPublishers.Add(publisher);
+
+ if (!didExist) {
+ DEBUG4("AddLocalPublisher: setting {} type {} typestr {}", topic->name,
+ static_cast<int>(config.type), config.typeStr);
+ // set the type to the published type
+ topic->type = config.type;
+ topic->typeStr = config.typeStr;
+ RefreshPubSubActive(topic, true);
+
+ if (properties.is_null()) {
+ topic->properties = wpi::json::object();
+ } else if (properties.is_object()) {
+ topic->properties = properties;
+ } else {
+ WARNING("ignoring non-object properties when publishing '{}'",
+ topic->name);
+ topic->properties = wpi::json::object();
+ }
+
+ if (topic->properties.empty()) {
+ NotifyTopic(topic, NT_EVENT_PUBLISH);
+ } else {
+ PropertiesUpdated(topic, topic->properties, NT_EVENT_PUBLISH, false);
+ }
+ } else {
+ // only need to update just this publisher
+ publisher->UpdateActive();
+ if (!publisher->active) {
+ // warn on type mismatch
+ INFO(
+ "local publish to '{}' disabled due to type mismatch (wanted '{}', "
+ "currently '{}')",
+ topic->name, config.typeStr, topic->typeStr);
+ }
+ }
+
+ if (publisher->active && m_network) {
+ m_network->Publish(publisher->handle, topic->handle, topic->name,
+ topic->typeStr, topic->properties, config);
+ }
+ return publisher;
+}
+
+std::unique_ptr<PublisherData> LSImpl::RemoveLocalPublisher(
+ NT_Publisher pubHandle) {
+ auto publisher = m_publishers.Remove(pubHandle);
+ if (publisher) {
+ auto topic = publisher->topic;
+ bool didExist = topic->Exists();
+ topic->localPublishers.Remove(publisher.get());
+ if (didExist && !topic->Exists()) {
+ CheckReset(topic);
+ NotifyTopic(topic, NT_EVENT_UNPUBLISH);
+ }
+
+ if (publisher->active && m_network) {
+ m_network->Unpublish(publisher->handle, topic->handle);
+ }
+
+ if (publisher->active && !topic->localPublishers.empty()) {
+ // some other publisher still exists; if it has a different type, refresh
+ // and publish it over the network
+ auto& nextPub = topic->localPublishers.front();
+ if (nextPub->config.type != topic->type ||
+ nextPub->config.typeStr != topic->typeStr) {
+ topic->type = nextPub->config.type;
+ topic->typeStr = nextPub->config.typeStr;
+ RefreshPubSubActive(topic, false);
+ if (nextPub->active && m_network) {
+ m_network->Publish(nextPub->handle, topic->handle, topic->name,
+ topic->typeStr, topic->properties,
+ nextPub->config);
+ }
+ }
+ }
+ }
+ return publisher;
+}
+
+SubscriberData* LSImpl::AddLocalSubscriber(TopicData* topic,
+ const PubSubConfig& config) {
+ DEBUG4("AddLocalSubscriber({})", topic->name);
+ auto subscriber = m_subscribers.Add(m_inst, topic, config);
+ topic->localSubscribers.Add(subscriber);
+ // set subscriber to active if the type matches
+ subscriber->UpdateActive();
+ if (topic->Exists() && !subscriber->active) {
+ // warn on type mismatch
+ INFO(
+ "local subscribe to '{}' disabled due to type mismatch (wanted '{}', "
+ "published as '{}')",
+ topic->name, config.typeStr, topic->typeStr);
+ }
+ if (m_network) {
+ DEBUG4("-> NetworkSubscribe({})", topic->name);
+ m_network->Subscribe(subscriber->handle, {{topic->name}}, config);
+ }
+ return subscriber;
+}
+
+std::unique_ptr<SubscriberData> LSImpl::RemoveLocalSubscriber(
+ NT_Subscriber subHandle) {
+ auto subscriber = m_subscribers.Remove(subHandle);
+ if (subscriber) {
+ auto topic = subscriber->topic;
+ topic->localSubscribers.Remove(subscriber.get());
+ for (auto&& listener : m_listeners) {
+ if (listener.getSecond()->subscriber == subscriber.get()) {
+ listener.getSecond()->subscriber = nullptr;
+ }
+ }
+ if (m_network) {
+ m_network->Unsubscribe(subscriber->handle);
+ }
+ }
+ return subscriber;
+}
+
+EntryData* LSImpl::AddEntry(SubscriberData* subscriber) {
+ auto entry = m_entries.Add(m_inst, subscriber);
+ subscriber->topic->entries.Add(entry);
+ return entry;
+}
+
+std::unique_ptr<EntryData> LSImpl::RemoveEntry(NT_Entry entryHandle) {
+ auto entry = m_entries.Remove(entryHandle);
+ if (entry) {
+ entry->topic->entries.Remove(entry.get());
+ }
+ return entry;
+}
+
+MultiSubscriberData* LSImpl::AddMultiSubscriber(
+ std::span<const std::string_view> prefixes, const PubSubOptions& options) {
+ auto subscriber = m_multiSubscribers.Add(m_inst, prefixes, options);
+ // subscribe to any already existing topics
+ for (auto&& topic : m_topics) {
+ for (auto&& prefix : prefixes) {
+ if (PrefixMatch(topic->name, prefix, topic->special)) {
+ topic->multiSubscribers.Add(subscriber);
+ break;
+ }
+ }
+ }
+ if (m_network) {
+ m_network->Subscribe(subscriber->handle, subscriber->prefixes,
+ subscriber->options);
+ }
+ return subscriber;
+}
+
+std::unique_ptr<MultiSubscriberData> LSImpl::RemoveMultiSubscriber(
+ NT_MultiSubscriber subHandle) {
+ auto subscriber = m_multiSubscribers.Remove(subHandle);
+ if (subscriber) {
+ for (auto&& topic : m_topics) {
+ topic->multiSubscribers.Remove(subscriber.get());
+ }
+ for (auto&& listener : m_listeners) {
+ if (listener.getSecond()->multiSubscriber == subscriber.get()) {
+ listener.getSecond()->multiSubscriber = nullptr;
+ }
+ }
+ if (m_network) {
+ m_network->Unsubscribe(subscriber->handle);
+ }
+ }
+ return subscriber;
+}
+
+void LSImpl::AddListenerImpl(NT_Listener listenerHandle, TopicData* topic,
+ unsigned int eventMask) {
+ if (topic->localSubscribers.size() >= kMaxSubscribers) {
+ ERROR(
+ "reached maximum number of subscribers to '{}', ignoring listener add",
+ topic->name);
+ return;
+ }
+ // subscribe to make sure topic updates are received
+ PubSubConfig config;
+ config.topicsOnly = (eventMask & NT_EVENT_VALUE_ALL) == 0;
+ auto sub = AddLocalSubscriber(topic, config);
+ AddListenerImpl(listenerHandle, sub, eventMask, sub->handle, true);
+}
+
+void LSImpl::AddListenerImpl(NT_Listener listenerHandle,
+ SubscriberData* subscriber, unsigned int eventMask,
+ NT_Handle subentryHandle, bool subscriberOwned) {
+ m_listeners.try_emplace(listenerHandle, std::make_unique<ListenerData>(
+ listenerHandle, subscriber,
+ eventMask, subscriberOwned));
+
+ auto topic = subscriber->topic;
+
+ if ((eventMask & NT_EVENT_TOPIC) != 0) {
+ if (topic->listeners.size() >= kMaxListeners) {
+ ERROR("reached maximum number of listeners to '{}', not adding listener",
+ topic->name);
+ return;
+ }
+
+ m_listenerStorage.Activate(
+ listenerHandle, eventMask & (NT_EVENT_TOPIC | NT_EVENT_IMMEDIATE));
+
+ topic->listeners.Add(listenerHandle);
+
+ // handle immediate publish
+ if ((eventMask & (NT_EVENT_PUBLISH | NT_EVENT_IMMEDIATE)) ==
+ (NT_EVENT_PUBLISH | NT_EVENT_IMMEDIATE) &&
+ topic->Exists()) {
+ m_listenerStorage.Notify({&listenerHandle, 1},
+ NT_EVENT_PUBLISH | NT_EVENT_IMMEDIATE,
+ topic->GetTopicInfo());
+ }
+ }
+
+ if ((eventMask & NT_EVENT_VALUE_ALL) != 0) {
+ if (subscriber->valueListeners.size() >= kMaxListeners) {
+ ERROR("reached maximum number of listeners to '{}', not adding listener",
+ topic->name);
+ return;
+ }
+ m_listenerStorage.Activate(
+ listenerHandle, eventMask & (NT_EVENT_VALUE_ALL | NT_EVENT_IMMEDIATE),
+ [subentryHandle](unsigned int mask, Event* event) {
+ if (auto valueData = event->GetValueEventData()) {
+ valueData->subentry = subentryHandle;
+ }
+ return true;
+ });
+
+ subscriber->valueListeners.Add(listenerHandle);
+
+ // handle immediate value
+ if ((eventMask & NT_EVENT_VALUE_ALL) != 0 &&
+ (eventMask & NT_EVENT_IMMEDIATE) != 0 && topic->lastValue) {
+ m_listenerStorage.Notify({&listenerHandle, 1},
+ NT_EVENT_IMMEDIATE | NT_EVENT_VALUE_ALL,
+ topic->handle, subentryHandle, topic->lastValue);
+ }
+ }
+}
+
+void LSImpl::AddListenerImpl(NT_Listener listenerHandle,
+ MultiSubscriberData* subscriber,
+ unsigned int eventMask, bool subscriberOwned) {
+ auto listener =
+ m_listeners
+ .try_emplace(listenerHandle, std::make_unique<ListenerData>(
+ listenerHandle, subscriber,
+ eventMask, subscriberOwned))
+ .first->getSecond()
+ .get();
+
+ // if we're doing anything immediate, get the list of matching topics
+ wpi::SmallVector<TopicData*, 32> topics;
+ if ((eventMask & NT_EVENT_IMMEDIATE) != 0 &&
+ (eventMask & (NT_EVENT_PUBLISH | NT_EVENT_VALUE_ALL)) != 0) {
+ for (auto&& topic : m_topics) {
+ if (topic->Exists() && subscriber->Matches(topic->name, topic->special)) {
+ topics.emplace_back(topic.get());
+ }
+ }
+ }
+
+ if ((eventMask & NT_EVENT_TOPIC) != 0) {
+ if (m_topicPrefixListeners.size() >= kMaxListeners) {
+ ERROR("reached maximum number of listeners, not adding listener");
+ return;
+ }
+
+ m_listenerStorage.Activate(
+ listenerHandle, eventMask & (NT_EVENT_TOPIC | NT_EVENT_IMMEDIATE));
+
+ m_topicPrefixListeners.Add(listener);
+
+ // handle immediate publish
+ if ((eventMask & (NT_EVENT_PUBLISH | NT_EVENT_IMMEDIATE)) ==
+ (NT_EVENT_PUBLISH | NT_EVENT_IMMEDIATE)) {
+ std::vector<TopicInfo> topicInfos;
+ for (auto&& topic : topics) {
+ topicInfos.emplace_back(topic->GetTopicInfo());
+ }
+ if (!topicInfos.empty()) {
+ m_listenerStorage.Notify({&listenerHandle, 1},
+ NT_EVENT_PUBLISH | NT_EVENT_IMMEDIATE,
+ topicInfos);
+ }
+ }
+ }
+
+ if ((eventMask & NT_EVENT_VALUE_ALL) != 0) {
+ if (subscriber->valueListeners.size() >= kMaxListeners) {
+ ERROR("reached maximum number of listeners, not adding listener");
+ return;
+ }
+
+ m_listenerStorage.Activate(
+ listenerHandle, eventMask & (NT_EVENT_VALUE_ALL | NT_EVENT_IMMEDIATE),
+ [subentryHandle = subscriber->handle.GetHandle()](unsigned int mask,
+ Event* event) {
+ if (auto valueData = event->GetValueEventData()) {
+ valueData->subentry = subentryHandle;
+ }
+ return true;
+ });
+
+ subscriber->valueListeners.Add(listenerHandle);
+
+ // handle immediate value
+ if ((eventMask & NT_EVENT_VALUE_ALL) != 0 &&
+ (eventMask & NT_EVENT_IMMEDIATE) != 0) {
+ for (auto&& topic : topics) {
+ if (topic->lastValue) {
+ m_listenerStorage.Notify(
+ {&listenerHandle, 1}, NT_EVENT_VALUE_ALL | NT_EVENT_IMMEDIATE,
+ topic->handle, subscriber->handle, topic->lastValue);
+ }
+ }
+ }
+ }
+}
+
+void LSImpl::AddListener(NT_Listener listenerHandle,
+ std::span<const std::string_view> prefixes,
+ unsigned int eventMask) {
+ if (m_multiSubscribers.size() >= kMaxMultiSubscribers) {
+ ERROR("reached maximum number of multi-subscribers, not adding listener");
+ return;
+ }
+ // subscribe to make sure topic updates are received
+ auto sub = AddMultiSubscriber(
+ prefixes, {.topicsOnly = (eventMask & NT_EVENT_VALUE_ALL) == 0});
+ AddListenerImpl(listenerHandle, sub, eventMask, true);
+}
+
+void LSImpl::AddListener(NT_Listener listenerHandle, NT_Handle handle,
+ unsigned int mask) {
+ if (auto topic = m_topics.Get(handle)) {
+ AddListenerImpl(listenerHandle, topic, mask);
+ } else if (auto sub = m_multiSubscribers.Get(handle)) {
+ AddListenerImpl(listenerHandle, sub, mask, false);
+ } else if (auto sub = m_subscribers.Get(handle)) {
+ AddListenerImpl(listenerHandle, sub, mask, sub->handle, false);
+ } else if (auto entry = m_entries.Get(handle)) {
+ AddListenerImpl(listenerHandle, entry->subscriber, mask, entry->handle,
+ false);
+ }
+}
+
+void LSImpl::RemoveListener(NT_Listener listenerHandle, unsigned int mask) {
+ auto listenerIt = m_listeners.find(listenerHandle);
+ if (listenerIt == m_listeners.end()) {
+ return;
+ }
+ auto listener = std::move(listenerIt->getSecond());
+ m_listeners.erase(listenerIt);
+ if (!listener) {
+ return;
+ }
+
+ m_topicPrefixListeners.Remove(listener.get());
+ if (listener->subscriber) {
+ listener->subscriber->valueListeners.Remove(listenerHandle);
+ listener->subscriber->topic->listeners.Remove(listenerHandle);
+ if (listener->subscriberOwned) {
+ RemoveLocalSubscriber(listener->subscriber->handle);
+ }
+ }
+ if (listener->multiSubscriber) {
+ listener->multiSubscriber->valueListeners.Remove(listenerHandle);
+ if (listener->subscriberOwned) {
+ RemoveMultiSubscriber(listener->multiSubscriber->handle);
+ }
+ }
+}
+
+TopicData* LSImpl::GetOrCreateTopic(std::string_view name) {
+ auto& topic = m_nameTopics[name];
+ // create if it does not already exist
+ if (!topic) {
+ topic = m_topics.Add(m_inst, name);
+ // attach multi-subscribers
+ for (auto&& sub : m_multiSubscribers) {
+ if (sub->Matches(name, topic->special)) {
+ topic->multiSubscribers.Add(sub.get());
+ }
+ }
+ }
+ return topic;
+}
+
+TopicData* LSImpl::GetTopic(NT_Handle handle) {
+ switch (Handle{handle}.GetType()) {
+ case Handle::kEntry: {
+ if (auto entry = m_entries.Get(handle)) {
+ return entry->topic;
+ }
+ break;
+ }
+ case Handle::kSubscriber: {
+ if (auto subscriber = m_subscribers.Get(handle)) {
+ return subscriber->topic;
+ }
+ break;
+ }
+ case Handle::kPublisher: {
+ if (auto publisher = m_publishers.Get(handle)) {
+ return publisher->topic;
+ }
+ break;
+ }
+ case Handle::kTopic:
+ return m_topics.Get(handle);
+ default:
+ break;
+ }
+ return {};
+}
+
+SubscriberData* LSImpl::GetSubEntry(NT_Handle subentryHandle) {
+ Handle h{subentryHandle};
+ if (h.IsType(Handle::kSubscriber)) {
+ return m_subscribers.Get(subentryHandle);
+ } else if (h.IsType(Handle::kEntry)) {
+ auto entry = m_entries.Get(subentryHandle);
+ return entry ? entry->subscriber : nullptr;
+ } else {
+ return nullptr;
+ }
+}
+
+PublisherData* LSImpl::PublishEntry(EntryData* entry, NT_Type type) {
+ if (entry->publisher) {
+ return entry->publisher;
+ }
+ auto typeStr = TypeToString(type);
+ if (entry->subscriber->config.type == NT_UNASSIGNED) {
+ entry->subscriber->config.type = type;
+ entry->subscriber->config.typeStr = typeStr;
+ } else if (entry->subscriber->config.type != type ||
+ entry->subscriber->config.typeStr != typeStr) {
+ if (!IsNumericCompatible(type, entry->subscriber->config.type)) {
+ // don't allow dynamically changing the type of an entry
+ ERROR("cannot publish entry {} as type {}, previously subscribed as {}",
+ entry->topic->name, typeStr, entry->subscriber->config.typeStr);
+ return nullptr;
+ }
+ }
+ // create publisher
+ entry->publisher = AddLocalPublisher(entry->topic, wpi::json::object(),
+ entry->subscriber->config);
+ return entry->publisher;
+}
+
+Value* LSImpl::GetSubEntryValue(NT_Handle subentryHandle) {
+ if (auto subscriber = GetSubEntry(subentryHandle)) {
+ return &subscriber->topic->lastValue;
+ } else {
+ return nullptr;
+ }
+}
+
+bool LSImpl::PublishLocalValue(PublisherData* publisher, const Value& value,
+ bool force) {
+ if (!value) {
+ return false;
+ }
+ if (publisher->topic->type != NT_UNASSIGNED &&
+ publisher->topic->type != value.type()) {
+ if (IsNumericCompatible(publisher->topic->type, value.type())) {
+ return PublishLocalValue(
+ publisher, ConvertNumericValue(value, publisher->topic->type));
+ }
+ return false;
+ }
+ if (publisher->active) {
+ bool isDuplicate, isNetworkDuplicate;
+ if (force || publisher->config.keepDuplicates) {
+ isDuplicate = false;
+ isNetworkDuplicate = false;
+ } else {
+ isDuplicate = (publisher->topic->lastValue == value);
+ isNetworkDuplicate = (publisher->topic->lastValueNetwork == value);
+ }
+ if (!isNetworkDuplicate && m_network) {
+ publisher->topic->lastValueNetwork = value;
+ m_network->SetValue(publisher->handle, value);
+ }
+ return SetValue(publisher->topic, value, NT_EVENT_VALUE_LOCAL, isDuplicate,
+ publisher);
+ } else {
+ return false;
+ }
+}
+
+bool LSImpl::SetEntryValue(NT_Handle pubentryHandle, const Value& value) {
+ if (!value) {
+ return false;
+ }
+ auto publisher = m_publishers.Get(pubentryHandle);
+ if (!publisher) {
+ if (auto entry = m_entries.Get(pubentryHandle)) {
+ publisher = PublishEntry(entry, value.type());
+ if (entry->subscriber->config.excludeSelf) {
+ entry->subscriber->config.excludePublisher = publisher->handle;
+ }
+ }
+ if (!publisher) {
+ return false;
+ }
+ }
+ return PublishLocalValue(publisher, value);
+}
+
+bool LSImpl::SetDefaultEntryValue(NT_Handle pubsubentryHandle,
+ const Value& value) {
+ DEBUG4("SetDefaultEntryValue({}, {})", pubsubentryHandle,
+ static_cast<int>(value.type()));
+ if (!value) {
+ return false;
+ }
+ if (auto topic = GetTopic(pubsubentryHandle)) {
+ if (!topic->lastValue &&
+ (topic->type == NT_UNASSIGNED || topic->type == value.type() ||
+ IsNumericCompatible(topic->type, value.type()))) {
+ // publish if we haven't yet
+ auto publisher = m_publishers.Get(pubsubentryHandle);
+ if (!publisher) {
+ if (auto entry = m_entries.Get(pubsubentryHandle)) {
+ publisher = PublishEntry(entry, value.type());
+ }
+ }
+
+ // force value timestamps to 0
+ if (topic->type == NT_UNASSIGNED) {
+ topic->type = value.type();
+ }
+ if (topic->type == value.type()) {
+ topic->lastValue = value;
+ } else if (IsNumericCompatible(topic->type, value.type())) {
+ topic->lastValue = ConvertNumericValue(value, topic->type);
+ } else {
+ return true;
+ }
+ topic->lastValue.SetTime(0);
+ topic->lastValue.SetServerTime(0);
+ if (publisher) {
+ PublishLocalValue(publisher, topic->lastValue, true);
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+void LSImpl::RemoveSubEntry(NT_Handle subentryHandle) {
+ Handle h{subentryHandle};
+ if (h.IsType(Handle::kSubscriber)) {
+ RemoveLocalSubscriber(subentryHandle);
+ } else if (h.IsType(Handle::kMultiSubscriber)) {
+ RemoveMultiSubscriber(subentryHandle);
+ } else if (h.IsType(Handle::kEntry)) {
+ if (auto entry = RemoveEntry(subentryHandle)) {
+ RemoveLocalSubscriber(entry->subscriber->handle);
+ if (entry->publisher) {
+ RemoveLocalPublisher(entry->publisher->handle);
+ }
+ }
+ }
+}
+
+class LocalStorage::Impl : public LSImpl {
+ public:
+ Impl(int inst, IListenerStorage& listenerStorage, wpi::Logger& logger)
+ : LSImpl{inst, listenerStorage, logger} {}
+};
+
+LocalStorage::LocalStorage(int inst, IListenerStorage& listenerStorage,
+ wpi::Logger& logger)
+ : m_impl{std::make_unique<Impl>(inst, listenerStorage, logger)} {}
+
+LocalStorage::~LocalStorage() = default;
+
+NT_Topic LocalStorage::NetworkAnnounce(std::string_view name,
+ std::string_view typeStr,
+ const wpi::json& properties,
+ NT_Publisher pubHandle) {
+ std::scoped_lock lock{m_mutex};
+ auto topic = m_impl->GetOrCreateTopic(name);
+ m_impl->NetworkAnnounce(topic, typeStr, properties, pubHandle);
+ return topic->handle;
+}
+
+void LocalStorage::NetworkUnannounce(std::string_view name) {
+ std::scoped_lock lock{m_mutex};
+ auto topic = m_impl->GetOrCreateTopic(name);
+ m_impl->RemoveNetworkPublisher(topic);
+}
+
+void LocalStorage::NetworkPropertiesUpdate(std::string_view name,
+ const wpi::json& update, bool ack) {
+ std::scoped_lock lock{m_mutex};
+ auto it = m_impl->m_nameTopics.find(name);
+ if (it != m_impl->m_nameTopics.end()) {
+ m_impl->NetworkPropertiesUpdate(it->second, update, ack);
+ }
+}
+
+void LocalStorage::NetworkSetValue(NT_Topic topicHandle, const Value& value) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ if (m_impl->SetValue(topic, value, NT_EVENT_VALUE_REMOTE,
+ value == topic->lastValue, nullptr)) {
+ topic->lastValueNetwork = value;
+ }
+ }
+}
+
+void LocalStorage::StartNetwork(net::NetworkInterface* network) {
+ WPI_DEBUG4(m_impl->m_logger, "StartNetwork()");
+ std::scoped_lock lock{m_mutex};
+ m_impl->m_network = network;
+ // publish all active publishers to the network and send last values
+ // only send value once per topic
+ for (auto&& topic : m_impl->m_topics) {
+ PublisherData* anyPublisher = nullptr;
+ for (auto&& publisher : topic->localPublishers) {
+ if (publisher->active) {
+ network->Publish(publisher->handle, topic->handle, topic->name,
+ topic->typeStr, topic->properties, publisher->config);
+ anyPublisher = publisher;
+ }
+ }
+ if (anyPublisher && topic->lastValue) {
+ network->SetValue(anyPublisher->handle, topic->lastValue);
+ }
+ }
+ for (auto&& subscriber : m_impl->m_subscribers) {
+ network->Subscribe(subscriber->handle, {{subscriber->topic->name}},
+ subscriber->config);
+ }
+ for (auto&& subscriber : m_impl->m_multiSubscribers) {
+ network->Subscribe(subscriber->handle, subscriber->prefixes,
+ subscriber->options);
+ }
+}
+
+void LocalStorage::ClearNetwork() {
+ WPI_DEBUG4(m_impl->m_logger, "ClearNetwork()");
+ std::scoped_lock lock{m_mutex};
+ m_impl->m_network = nullptr;
+ // treat as an unannounce all from the network side
+ for (auto&& topic : m_impl->m_topics) {
+ m_impl->RemoveNetworkPublisher(topic.get());
+ }
+}
+
+template <typename T, typename F>
+static void ForEachTopic(T& topics, std::string_view prefix, unsigned int types,
+ F func) {
+ for (auto&& topic : topics) {
+ if (!topic->Exists()) {
+ continue;
+ }
+ if (!wpi::starts_with(topic->name, prefix)) {
+ continue;
+ }
+ if (types != 0 && (types & topic->type) == 0) {
+ continue;
+ }
+ func(*topic);
+ }
+}
+
+template <typename T, typename F>
+static void ForEachTopic(T& topics, std::string_view prefix,
+ std::span<const std::string_view> types, F func) {
+ for (auto&& topic : topics) {
+ if (!topic->Exists()) {
+ continue;
+ }
+ if (!wpi::starts_with(topic->name, prefix)) {
+ continue;
+ }
+ if (!types.empty()) {
+ bool match = false;
+ for (auto&& type : types) {
+ if (topic->typeStr == type) {
+ match = true;
+ break;
+ }
+ }
+ if (!match) {
+ continue;
+ }
+ }
+ func(*topic);
+ }
+}
+
+std::vector<NT_Topic> LocalStorage::GetTopics(std::string_view prefix,
+ unsigned int types) {
+ std::scoped_lock lock(m_mutex);
+ std::vector<NT_Topic> rv;
+ ForEachTopic(m_impl->m_topics, prefix, types,
+ [&](TopicData& topic) { rv.push_back(topic.handle); });
+ return rv;
+}
+
+std::vector<NT_Topic> LocalStorage::GetTopics(
+ std::string_view prefix, std::span<const std::string_view> types) {
+ std::scoped_lock lock(m_mutex);
+ std::vector<NT_Topic> rv;
+ ForEachTopic(m_impl->m_topics, prefix, types,
+ [&](TopicData& topic) { rv.push_back(topic.handle); });
+ return rv;
+}
+
+std::vector<TopicInfo> LocalStorage::GetTopicInfo(std::string_view prefix,
+ unsigned int types) {
+ std::scoped_lock lock(m_mutex);
+ std::vector<TopicInfo> rv;
+ ForEachTopic(m_impl->m_topics, prefix, types, [&](TopicData& topic) {
+ rv.emplace_back(topic.GetTopicInfo());
+ });
+ return rv;
+}
+
+std::vector<TopicInfo> LocalStorage::GetTopicInfo(
+ std::string_view prefix, std::span<const std::string_view> types) {
+ std::scoped_lock lock(m_mutex);
+ std::vector<TopicInfo> rv;
+ ForEachTopic(m_impl->m_topics, prefix, types, [&](TopicData& topic) {
+ rv.emplace_back(topic.GetTopicInfo());
+ });
+ return rv;
+}
+
+NT_Topic LocalStorage::GetTopic(std::string_view name) {
+ if (name.empty()) {
+ return {};
+ }
+ std::scoped_lock lock{m_mutex};
+ return m_impl->GetOrCreateTopic(name)->handle;
+}
+
+std::string LocalStorage::GetTopicName(NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return topic->name;
+ } else {
+ return {};
+ }
+}
+
+NT_Type LocalStorage::GetTopicType(NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return topic->type;
+ } else {
+ return {};
+ }
+}
+
+std::string LocalStorage::GetTopicTypeString(NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return topic->typeStr;
+ } else {
+ return {};
+ }
+}
+
+void LocalStorage::SetTopicPersistent(NT_Topic topicHandle, bool value) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ m_impl->SetPersistent(topic, value);
+ }
+}
+
+bool LocalStorage::GetTopicPersistent(NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return (topic->flags & NT_PERSISTENT) != 0;
+ } else {
+ return false;
+ }
+}
+
+void LocalStorage::SetTopicRetained(NT_Topic topicHandle, bool value) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ m_impl->SetRetained(topic, value);
+ }
+}
+
+bool LocalStorage::GetTopicRetained(NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return (topic->flags & NT_RETAINED) != 0;
+ } else {
+ return false;
+ }
+}
+
+bool LocalStorage::GetTopicExists(NT_Handle handle) {
+ std::scoped_lock lock{m_mutex};
+ TopicData* topic = m_impl->GetTopic(handle);
+ return topic && topic->Exists();
+}
+
+wpi::json LocalStorage::GetTopicProperty(NT_Topic topicHandle,
+ std::string_view name) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return topic->properties.value(name, wpi::json{});
+ } else {
+ return {};
+ }
+}
+
+void LocalStorage::SetTopicProperty(NT_Topic topicHandle, std::string_view name,
+ const wpi::json& value) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ if (value.is_null()) {
+ topic->properties.erase(name);
+ } else {
+ topic->properties[name] = value;
+ }
+ wpi::json update = wpi::json::object();
+ update[name] = value;
+ m_impl->PropertiesUpdated(topic, update, NT_EVENT_NONE, true);
+ }
+}
+
+void LocalStorage::DeleteTopicProperty(NT_Topic topicHandle,
+ std::string_view name) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ topic->properties.erase(name);
+ wpi::json update = wpi::json::object();
+ update[name] = wpi::json();
+ m_impl->PropertiesUpdated(topic, update, NT_EVENT_NONE, true);
+ }
+}
+
+wpi::json LocalStorage::GetTopicProperties(NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return topic->properties;
+ } else {
+ return wpi::json::object();
+ }
+}
+
+bool LocalStorage::SetTopicProperties(NT_Topic topicHandle,
+ const wpi::json& update) {
+ if (!update.is_object()) {
+ return false;
+ }
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ m_impl->SetProperties(topic, update, true);
+ return true;
+ } else {
+ return {};
+ }
+}
+
+TopicInfo LocalStorage::GetTopicInfo(NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->m_topics.Get(topicHandle)) {
+ return topic->GetTopicInfo();
+ } else {
+ return {};
+ }
+}
+
+NT_Subscriber LocalStorage::Subscribe(NT_Topic topicHandle, NT_Type type,
+ std::string_view typeStr,
+ const PubSubOptions& options) {
+ std::scoped_lock lock{m_mutex};
+
+ // Get the topic
+ auto* topic = m_impl->m_topics.Get(topicHandle);
+ if (!topic) {
+ return 0;
+ }
+
+ if (topic->localSubscribers.size() >= kMaxSubscribers) {
+ WPI_ERROR(m_impl->m_logger,
+ "reached maximum number of subscribers to '{}', not subscribing",
+ topic->name);
+ return 0;
+ }
+
+ // Create subscriber
+ return m_impl->AddLocalSubscriber(topic, PubSubConfig{type, typeStr, options})
+ ->handle;
+}
+
+void LocalStorage::Unsubscribe(NT_Subscriber subHandle) {
+ std::scoped_lock lock{m_mutex};
+ m_impl->RemoveSubEntry(subHandle);
+}
+
+NT_MultiSubscriber LocalStorage::SubscribeMultiple(
+ std::span<const std::string_view> prefixes, const PubSubOptions& options) {
+ std::scoped_lock lock{m_mutex};
+
+ if (m_impl->m_multiSubscribers.size() >= kMaxMultiSubscribers) {
+ WPI_ERROR(m_impl->m_logger,
+ "reached maximum number of multi-subscribers, not subscribing");
+ return 0;
+ }
+
+ return m_impl->AddMultiSubscriber(prefixes, options)->handle;
+}
+
+void LocalStorage::UnsubscribeMultiple(NT_MultiSubscriber subHandle) {
+ std::scoped_lock lock{m_mutex};
+ m_impl->RemoveMultiSubscriber(subHandle);
+}
+
+NT_Publisher LocalStorage::Publish(NT_Topic topicHandle, NT_Type type,
+ std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptions& options) {
+ std::scoped_lock lock{m_mutex};
+
+ // Get the topic
+ auto* topic = m_impl->m_topics.Get(topicHandle);
+ if (!topic) {
+ WPI_ERROR(m_impl->m_logger, "trying to publish invalid topic handle ({})",
+ topicHandle);
+ return 0;
+ }
+
+ if (type == NT_UNASSIGNED || typeStr.empty()) {
+ WPI_ERROR(
+ m_impl->m_logger,
+ "cannot publish '{}' with an unassigned type or empty type string",
+ topic->name);
+ return 0;
+ }
+
+ if (topic->localPublishers.size() >= kMaxPublishers) {
+ WPI_ERROR(m_impl->m_logger,
+ "reached maximum number of publishers to '{}', not publishing",
+ topic->name);
+ return 0;
+ }
+
+ return m_impl
+ ->AddLocalPublisher(topic, properties,
+ PubSubConfig{type, typeStr, options})
+ ->handle;
+}
+
+void LocalStorage::Unpublish(NT_Handle pubentryHandle) {
+ std::scoped_lock lock{m_mutex};
+
+ if (Handle{pubentryHandle}.IsType(Handle::kPublisher)) {
+ m_impl->RemoveLocalPublisher(pubentryHandle);
+ } else if (auto entry = m_impl->m_entries.Get(pubentryHandle)) {
+ if (entry->publisher) {
+ m_impl->RemoveLocalPublisher(entry->publisher->handle);
+ entry->publisher = nullptr;
+ }
+ } else {
+ // TODO: report warning
+ return;
+ }
+}
+
+NT_Entry LocalStorage::GetEntry(NT_Topic topicHandle, NT_Type type,
+ std::string_view typeStr,
+ const PubSubOptions& options) {
+ std::scoped_lock lock{m_mutex};
+
+ // Get the topic
+ auto* topic = m_impl->m_topics.Get(topicHandle);
+ if (!topic) {
+ return 0;
+ }
+
+ if (topic->localSubscribers.size() >= kMaxSubscribers) {
+ WPI_ERROR(
+ m_impl->m_logger,
+ "reached maximum number of subscribers to '{}', not creating entry",
+ topic->name);
+ return 0;
+ }
+
+ // Create subscriber
+ auto subscriber =
+ m_impl->AddLocalSubscriber(topic, PubSubConfig{type, typeStr, options});
+
+ // Create entry
+ return m_impl->AddEntry(subscriber)->handle;
+}
+
+void LocalStorage::ReleaseEntry(NT_Entry entryHandle) {
+ std::scoped_lock lock{m_mutex};
+ m_impl->RemoveSubEntry(entryHandle);
+}
+
+void LocalStorage::Release(NT_Handle pubsubentryHandle) {
+ switch (Handle{pubsubentryHandle}.GetType()) {
+ case Handle::kEntry:
+ ReleaseEntry(pubsubentryHandle);
+ break;
+ case Handle::kPublisher:
+ Unpublish(pubsubentryHandle);
+ break;
+ case Handle::kSubscriber:
+ Unsubscribe(pubsubentryHandle);
+ break;
+ case Handle::kMultiSubscriber:
+ UnsubscribeMultiple(pubsubentryHandle);
+ break;
+ default:
+ break;
+ }
+}
+
+NT_Topic LocalStorage::GetTopicFromHandle(NT_Handle pubsubentryHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto topic = m_impl->GetTopic(pubsubentryHandle)) {
+ return topic->handle;
+ } else {
+ return {};
+ }
+}
+
+bool LocalStorage::SetEntryValue(NT_Handle pubentryHandle, const Value& value) {
+ std::scoped_lock lock{m_mutex};
+ return m_impl->SetEntryValue(pubentryHandle, value);
+}
+
+bool LocalStorage::SetDefaultEntryValue(NT_Handle pubsubentryHandle,
+ const Value& value) {
+ std::scoped_lock lock{m_mutex};
+ return m_impl->SetDefaultEntryValue(pubsubentryHandle, value);
+}
+
+TimestampedBoolean LocalStorage::GetAtomicBoolean(NT_Handle subentryHandle,
+ bool defaultValue) {
+ std::scoped_lock lock{m_mutex};
+ Value* value = m_impl->GetSubEntryValue(subentryHandle);
+ if (value && value->type() == NT_BOOLEAN) {
+ return {value->time(), value->server_time(), value->GetBoolean()};
+ } else {
+ return {0, 0, defaultValue};
+ }
+}
+
+TimestampedString LocalStorage::GetAtomicString(NT_Handle subentryHandle,
+ std::string_view defaultValue) {
+ std::scoped_lock lock{m_mutex};
+ Value* value = m_impl->GetSubEntryValue(subentryHandle);
+ if (value && value->type() == NT_STRING) {
+ return {value->time(), value->server_time(),
+ std::string{value->GetString()}};
+ } else {
+ return {0, 0, std::string{defaultValue}};
+ }
+}
+
+TimestampedStringView LocalStorage::GetAtomicString(
+ NT_Handle subentryHandle, wpi::SmallVectorImpl<char>& buf,
+ std::string_view defaultValue) {
+ std::scoped_lock lock{m_mutex};
+ Value* value = m_impl->GetSubEntryValue(subentryHandle);
+ if (value && value->type() == NT_STRING) {
+ auto str = value->GetString();
+ buf.assign(str.begin(), str.end());
+ return {value->time(), value->server_time(), {buf.data(), buf.size()}};
+ } else {
+ return {0, 0, defaultValue};
+ }
+}
+
+template <typename T, typename U>
+static T GetAtomicNumber(Value* value, U defaultValue) {
+ if (value && value->type() == NT_INTEGER) {
+ return {value->time(), value->server_time(),
+ static_cast<U>(value->GetInteger())};
+ } else if (value && value->type() == NT_FLOAT) {
+ return {value->time(), value->server_time(),
+ static_cast<U>(value->GetFloat())};
+ } else if (value && value->type() == NT_DOUBLE) {
+ return {value->time(), value->server_time(),
+ static_cast<U>(value->GetDouble())};
+ } else {
+ return {0, 0, defaultValue};
+ }
+}
+
+template <typename T, typename U>
+static T GetAtomicNumberArray(Value* value, std::span<const U> defaultValue) {
+ if (value && value->type() == NT_INTEGER_ARRAY) {
+ auto arr = value->GetIntegerArray();
+ return {value->time(), value->server_time(), {arr.begin(), arr.end()}};
+ } else if (value && value->type() == NT_FLOAT_ARRAY) {
+ auto arr = value->GetFloatArray();
+ return {value->time(), value->server_time(), {arr.begin(), arr.end()}};
+ } else if (value && value->type() == NT_DOUBLE_ARRAY) {
+ auto arr = value->GetDoubleArray();
+ return {value->time(), value->server_time(), {arr.begin(), arr.end()}};
+ } else {
+ return {0, 0, {defaultValue.begin(), defaultValue.end()}};
+ }
+}
+
+template <typename T, typename U>
+static T GetAtomicNumberArray(Value* value, wpi::SmallVectorImpl<U>& buf,
+ std::span<const U> defaultValue) {
+ if (value && value->type() == NT_INTEGER_ARRAY) {
+ auto str = value->GetIntegerArray();
+ buf.assign(str.begin(), str.end());
+ return {value->time(), value->server_time(), {buf.data(), buf.size()}};
+ } else if (value && value->type() == NT_FLOAT_ARRAY) {
+ auto str = value->GetFloatArray();
+ buf.assign(str.begin(), str.end());
+ return {value->time(), value->server_time(), {buf.data(), buf.size()}};
+ } else if (value && value->type() == NT_DOUBLE_ARRAY) {
+ auto str = value->GetDoubleArray();
+ buf.assign(str.begin(), str.end());
+ return {value->time(), value->server_time(), {buf.data(), buf.size()}};
+ } else {
+ buf.assign(defaultValue.begin(), defaultValue.end());
+ return {0, 0, {buf.data(), buf.size()}};
+ }
+}
+
+#define GET_ATOMIC_NUMBER(Name, dtype) \
+ Timestamped##Name LocalStorage::GetAtomic##Name(NT_Handle subentry, \
+ dtype defaultValue) { \
+ std::scoped_lock lock{m_mutex}; \
+ return GetAtomicNumber<Timestamped##Name>( \
+ m_impl->GetSubEntryValue(subentry), defaultValue); \
+ } \
+ \
+ Timestamped##Name##Array LocalStorage::GetAtomic##Name##Array( \
+ NT_Handle subentry, std::span<const dtype> defaultValue) { \
+ std::scoped_lock lock{m_mutex}; \
+ return GetAtomicNumberArray<Timestamped##Name##Array>( \
+ m_impl->GetSubEntryValue(subentry), defaultValue); \
+ } \
+ \
+ Timestamped##Name##ArrayView LocalStorage::GetAtomic##Name##Array( \
+ NT_Handle subentry, wpi::SmallVectorImpl<dtype>& buf, \
+ std::span<const dtype> defaultValue) { \
+ std::scoped_lock lock{m_mutex}; \
+ return GetAtomicNumberArray<Timestamped##Name##ArrayView>( \
+ m_impl->GetSubEntryValue(subentry), buf, defaultValue); \
+ }
+
+GET_ATOMIC_NUMBER(Integer, int64_t)
+GET_ATOMIC_NUMBER(Float, float)
+GET_ATOMIC_NUMBER(Double, double)
+
+#define GET_ATOMIC_ARRAY(Name, dtype) \
+ Timestamped##Name LocalStorage::GetAtomic##Name( \
+ NT_Handle subentry, std::span<const dtype> defaultValue) { \
+ std::scoped_lock lock{m_mutex}; \
+ Value* value = m_impl->GetSubEntryValue(subentry); \
+ if (value && value->Is##Name()) { \
+ auto arr = value->Get##Name(); \
+ return {value->time(), value->server_time(), {arr.begin(), arr.end()}}; \
+ } else { \
+ return {0, 0, {defaultValue.begin(), defaultValue.end()}}; \
+ } \
+ }
+
+GET_ATOMIC_ARRAY(Raw, uint8_t)
+GET_ATOMIC_ARRAY(BooleanArray, int)
+GET_ATOMIC_ARRAY(StringArray, std::string)
+
+#define GET_ATOMIC_SMALL_ARRAY(Name, dtype) \
+ Timestamped##Name##View LocalStorage::GetAtomic##Name( \
+ NT_Handle subentry, wpi::SmallVectorImpl<dtype>& buf, \
+ std::span<const dtype> defaultValue) { \
+ std::scoped_lock lock{m_mutex}; \
+ Value* value = m_impl->GetSubEntryValue(subentry); \
+ if (value && value->Is##Name()) { \
+ auto str = value->Get##Name(); \
+ buf.assign(str.begin(), str.end()); \
+ return {value->time(), value->server_time(), {buf.data(), buf.size()}}; \
+ } else { \
+ buf.assign(defaultValue.begin(), defaultValue.end()); \
+ return {0, 0, {buf.data(), buf.size()}}; \
+ } \
+ }
+
+GET_ATOMIC_SMALL_ARRAY(Raw, uint8_t)
+GET_ATOMIC_SMALL_ARRAY(BooleanArray, int)
+
+std::vector<Value> LocalStorage::ReadQueueValue(NT_Handle subentry) {
+ std::scoped_lock lock{m_mutex};
+ auto subscriber = m_impl->GetSubEntry(subentry);
+ if (!subscriber) {
+ return {};
+ }
+ std::vector<Value> rv;
+ rv.reserve(subscriber->pollStorage.size());
+ for (auto&& val : subscriber->pollStorage) {
+ rv.emplace_back(std::move(val));
+ }
+ subscriber->pollStorage.reset();
+ return rv;
+}
+
+std::vector<TimestampedBoolean> LocalStorage::ReadQueueBoolean(
+ NT_Handle subentry) {
+ std::scoped_lock lock{m_mutex};
+ auto subscriber = m_impl->GetSubEntry(subentry);
+ if (!subscriber) {
+ return {};
+ }
+ std::vector<TimestampedBoolean> rv;
+ rv.reserve(subscriber->pollStorage.size());
+ for (auto&& val : subscriber->pollStorage) {
+ if (val.IsBoolean()) {
+ rv.emplace_back(val.time(), val.server_time(), val.GetBoolean());
+ }
+ }
+ subscriber->pollStorage.reset();
+ return rv;
+}
+
+std::vector<TimestampedString> LocalStorage::ReadQueueString(
+ NT_Handle subentry) {
+ std::scoped_lock lock{m_mutex};
+ auto subscriber = m_impl->GetSubEntry(subentry);
+ if (!subscriber) {
+ return {};
+ }
+ std::vector<TimestampedString> rv;
+ rv.reserve(subscriber->pollStorage.size());
+ for (auto&& val : subscriber->pollStorage) {
+ if (val.IsString()) {
+ rv.emplace_back(val.time(), val.server_time(),
+ std::string{val.GetString()});
+ }
+ }
+ subscriber->pollStorage.reset();
+ return rv;
+}
+
+#define READ_QUEUE_ARRAY(Name) \
+ std::vector<Timestamped##Name> LocalStorage::ReadQueue##Name( \
+ NT_Handle subentry) { \
+ std::scoped_lock lock{m_mutex}; \
+ auto subscriber = m_impl->GetSubEntry(subentry); \
+ if (!subscriber) { \
+ return {}; \
+ } \
+ std::vector<Timestamped##Name> rv; \
+ rv.reserve(subscriber->pollStorage.size()); \
+ for (auto&& val : subscriber->pollStorage) { \
+ if (val.Is##Name()) { \
+ auto arr = val.Get##Name(); \
+ rv.emplace_back(Timestamped##Name{ \
+ val.time(), val.server_time(), {arr.begin(), arr.end()}}); \
+ } \
+ } \
+ subscriber->pollStorage.reset(); \
+ return rv; \
+ }
+
+READ_QUEUE_ARRAY(Raw)
+READ_QUEUE_ARRAY(BooleanArray)
+READ_QUEUE_ARRAY(StringArray)
+
+template <typename T>
+static std::vector<T> ReadQueueNumber(SubscriberData* subscriber) {
+ if (!subscriber) {
+ return {};
+ }
+ std::vector<T> rv;
+ rv.reserve(subscriber->pollStorage.size());
+ for (auto&& val : subscriber->pollStorage) {
+ auto ts = val.time();
+ auto sts = val.server_time();
+ if (val.IsInteger()) {
+ rv.emplace_back(T(ts, sts, val.GetInteger()));
+ } else if (val.IsFloat()) {
+ rv.emplace_back(T(ts, sts, val.GetFloat()));
+ } else if (val.IsDouble()) {
+ rv.emplace_back(T(ts, sts, val.GetDouble()));
+ }
+ }
+ subscriber->pollStorage.reset();
+ return rv;
+}
+
+template <typename T>
+static std::vector<T> ReadQueueNumberArray(SubscriberData* subscriber) {
+ if (!subscriber) {
+ return {};
+ }
+ std::vector<T> rv;
+ rv.reserve(subscriber->pollStorage.size());
+ for (auto&& val : subscriber->pollStorage) {
+ auto ts = val.time();
+ auto sts = val.server_time();
+ if (val.IsIntegerArray()) {
+ auto arr = val.GetIntegerArray();
+ rv.emplace_back(T{ts, sts, {arr.begin(), arr.end()}});
+ } else if (val.IsFloatArray()) {
+ auto arr = val.GetFloatArray();
+ rv.emplace_back(T{ts, sts, {arr.begin(), arr.end()}});
+ } else if (val.IsDoubleArray()) {
+ auto arr = val.GetDoubleArray();
+ rv.emplace_back(T{ts, sts, {arr.begin(), arr.end()}});
+ }
+ }
+ subscriber->pollStorage.reset();
+ return rv;
+}
+
+#define READ_QUEUE_NUMBER(Name) \
+ std::vector<Timestamped##Name> LocalStorage::ReadQueue##Name( \
+ NT_Handle subentry) { \
+ std::scoped_lock lock{m_mutex}; \
+ return ReadQueueNumber<Timestamped##Name>(m_impl->GetSubEntry(subentry)); \
+ } \
+ \
+ std::vector<Timestamped##Name##Array> LocalStorage::ReadQueue##Name##Array( \
+ NT_Handle subentry) { \
+ std::scoped_lock lock{m_mutex}; \
+ return ReadQueueNumberArray<Timestamped##Name##Array>( \
+ m_impl->GetSubEntry(subentry)); \
+ }
+
+READ_QUEUE_NUMBER(Integer)
+READ_QUEUE_NUMBER(Float)
+READ_QUEUE_NUMBER(Double)
+
+Value LocalStorage::GetEntryValue(NT_Handle subentryHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto subscriber = m_impl->GetSubEntry(subentryHandle)) {
+ if (subscriber->config.type == NT_UNASSIGNED ||
+ !subscriber->topic->lastValue ||
+ subscriber->config.type == subscriber->topic->lastValue.type()) {
+ return subscriber->topic->lastValue;
+ } else if (IsNumericCompatible(subscriber->config.type,
+ subscriber->topic->lastValue.type())) {
+ return ConvertNumericValue(subscriber->topic->lastValue,
+ subscriber->config.type);
+ }
+ }
+ return {};
+}
+
+void LocalStorage::SetEntryFlags(NT_Entry entryHandle, unsigned int flags) {
+ std::scoped_lock lock{m_mutex};
+ if (auto entry = m_impl->m_entries.Get(entryHandle)) {
+ m_impl->SetFlags(entry->subscriber->topic, flags);
+ }
+}
+
+unsigned int LocalStorage::GetEntryFlags(NT_Entry entryHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto entry = m_impl->m_entries.Get(entryHandle)) {
+ return entry->subscriber->topic->flags;
+ } else {
+ return 0;
+ }
+}
+
+NT_Entry LocalStorage::GetEntry(std::string_view name) {
+ if (name.empty()) {
+ return {};
+ }
+
+ std::scoped_lock lock{m_mutex};
+
+ // Get the topic data
+ auto* topic = m_impl->GetOrCreateTopic(name);
+
+ if (topic->entry == 0) {
+ if (topic->localSubscribers.size() >= kMaxSubscribers) {
+ WPI_ERROR(
+ m_impl->m_logger,
+ "reached maximum number of subscribers to '{}', not creating entry",
+ topic->name);
+ return 0;
+ }
+
+ // Create subscriber
+ auto* subscriber = m_impl->AddLocalSubscriber(topic, {});
+
+ // Create entry
+ topic->entry = m_impl->AddEntry(subscriber)->handle;
+ }
+
+ return topic->entry;
+}
+
+std::string LocalStorage::GetEntryName(NT_Handle subentryHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto subscriber = m_impl->GetSubEntry(subentryHandle)) {
+ return subscriber->topic->name;
+ } else {
+ return {};
+ }
+}
+
+NT_Type LocalStorage::GetEntryType(NT_Handle subentryHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto subscriber = m_impl->GetSubEntry(subentryHandle)) {
+ return subscriber->topic->type;
+ } else {
+ return {};
+ }
+}
+
+int64_t LocalStorage::GetEntryLastChange(NT_Handle subentryHandle) {
+ std::scoped_lock lock{m_mutex};
+ if (auto subscriber = m_impl->GetSubEntry(subentryHandle)) {
+ return subscriber->topic->lastValue.time();
+ } else {
+ return 0;
+ }
+}
+
+void LocalStorage::AddListener(NT_Listener listener,
+ std::span<const std::string_view> prefixes,
+ unsigned int mask) {
+ mask &= (NT_EVENT_TOPIC | NT_EVENT_VALUE_ALL | NT_EVENT_IMMEDIATE);
+ std::scoped_lock lock{m_mutex};
+ m_impl->AddListener(listener, prefixes, mask);
+}
+
+void LocalStorage::AddListener(NT_Listener listener, NT_Handle handle,
+ unsigned int mask) {
+ mask &= (NT_EVENT_TOPIC | NT_EVENT_VALUE_ALL | NT_EVENT_IMMEDIATE);
+ std::scoped_lock lock{m_mutex};
+ m_impl->AddListener(listener, handle, mask);
+}
+
+void LocalStorage::RemoveListener(NT_Listener listener, unsigned int mask) {
+ std::scoped_lock lock{m_mutex};
+ m_impl->RemoveListener(listener, mask);
+}
+
+NT_DataLogger LocalStorage::StartDataLog(wpi::log::DataLog& log,
+ std::string_view prefix,
+ std::string_view logPrefix) {
+ std::scoped_lock lock{m_mutex};
+ auto datalogger =
+ m_impl->m_dataloggers.Add(m_impl->m_inst, log, prefix, logPrefix);
+
+ // start logging any matching topics
+ auto now = nt::Now();
+ for (auto&& topic : m_impl->m_topics) {
+ if (!wpi::starts_with(topic->name, prefix) ||
+ topic->type == NT_UNASSIGNED || topic->typeStr.empty()) {
+ continue;
+ }
+ topic->datalogs.emplace_back(log, datalogger->Start(topic.get(), now),
+ datalogger->handle);
+
+ // log current value, if any
+ if (!topic->lastValue) {
+ continue;
+ }
+ topic->datalogType = topic->type;
+ topic->datalogs.back().Append(topic->lastValue);
+ }
+
+ return datalogger->handle;
+}
+
+void LocalStorage::StopDataLog(NT_DataLogger logger) {
+ std::scoped_lock lock{m_mutex};
+ if (auto datalogger = m_impl->m_dataloggers.Remove(logger)) {
+ // finish any active entries
+ auto now = Now();
+ for (auto&& topic : m_impl->m_topics) {
+ auto it =
+ std::find_if(topic->datalogs.begin(), topic->datalogs.end(),
+ [&](const auto& elem) { return elem.logger == logger; });
+ if (it != topic->datalogs.end()) {
+ it->log->Finish(it->entry, now);
+ topic->datalogs.erase(it);
+ }
+ }
+ }
+}
+
+void LocalStorage::Reset() {
+ std::scoped_lock lock{m_mutex};
+ m_impl = std::make_unique<Impl>(m_impl->m_inst, m_impl->m_listenerStorage,
+ m_impl->m_logger);
+}
diff --git a/ntcore/src/main/native/cpp/LocalStorage.h b/ntcore/src/main/native/cpp/LocalStorage.h
new file mode 100644
index 0000000..a93adb0
--- /dev/null
+++ b/ntcore/src/main/native/cpp/LocalStorage.h
@@ -0,0 +1,219 @@
+// 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 <functional>
+#include <memory>
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include <wpi/mutex.h>
+
+#include "net/NetworkInterface.h"
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class Logger;
+} // namespace wpi
+
+namespace nt {
+
+class IListenerStorage;
+
+class LocalStorage final : public net::ILocalStorage {
+ public:
+ LocalStorage(int inst, IListenerStorage& listenerStorage,
+ wpi::Logger& logger);
+ LocalStorage(const LocalStorage&) = delete;
+ LocalStorage& operator=(const LocalStorage&) = delete;
+ ~LocalStorage() final;
+
+ // network interface functions
+ NT_Topic NetworkAnnounce(std::string_view name, std::string_view typeStr,
+ const wpi::json& properties,
+ NT_Publisher pubHandle) final;
+ void NetworkUnannounce(std::string_view name) final;
+ void NetworkPropertiesUpdate(std::string_view name, const wpi::json& update,
+ bool ack) final;
+ void NetworkSetValue(NT_Topic topicHandle, const Value& value) final;
+
+ void StartNetwork(net::NetworkInterface* network) final;
+ void ClearNetwork() final;
+
+ // User functions. These are the actual implementations of the corresponding
+ // user API functions in ntcore_cpp.
+
+ std::vector<NT_Topic> GetTopics(std::string_view prefix, unsigned int types);
+ std::vector<NT_Topic> GetTopics(std::string_view prefix,
+ std::span<const std::string_view> types);
+
+ std::vector<TopicInfo> GetTopicInfo(std::string_view prefix,
+ unsigned int types);
+ std::vector<TopicInfo> GetTopicInfo(std::string_view prefix,
+ std::span<const std::string_view> types);
+
+ NT_Topic GetTopic(std::string_view name);
+
+ std::string GetTopicName(NT_Topic topic);
+
+ NT_Type GetTopicType(NT_Topic topic);
+
+ std::string GetTopicTypeString(NT_Topic topic);
+
+ void SetTopicPersistent(NT_Topic topic, bool value);
+
+ bool GetTopicPersistent(NT_Topic topic);
+
+ void SetTopicRetained(NT_Topic topic, bool value);
+
+ bool GetTopicRetained(NT_Topic topic);
+
+ bool GetTopicExists(NT_Handle handle);
+
+ wpi::json GetTopicProperty(NT_Topic topic, std::string_view name);
+
+ void SetTopicProperty(NT_Topic topic, std::string_view name,
+ const wpi::json& value);
+
+ void DeleteTopicProperty(NT_Topic topic, std::string_view name);
+
+ wpi::json GetTopicProperties(NT_Topic topic);
+
+ bool SetTopicProperties(NT_Topic topic, const wpi::json& update);
+
+ TopicInfo GetTopicInfo(NT_Topic topic);
+
+ NT_Subscriber Subscribe(NT_Topic topic, NT_Type type,
+ std::string_view typeStr,
+ const PubSubOptions& options);
+
+ void Unsubscribe(NT_Subscriber sub);
+
+ NT_MultiSubscriber SubscribeMultiple(
+ std::span<const std::string_view> prefixes, const PubSubOptions& options);
+
+ void UnsubscribeMultiple(NT_MultiSubscriber subHandle);
+
+ NT_Publisher Publish(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptions& options);
+
+ void Unpublish(NT_Handle pubentry);
+
+ NT_Entry GetEntry(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options);
+
+ void ReleaseEntry(NT_Entry entry);
+
+ void Release(NT_Handle pubsubentry);
+
+ NT_Topic GetTopicFromHandle(NT_Handle pubsubentry);
+
+ bool SetEntryValue(NT_Handle pubentry, const Value& value);
+
+ bool SetDefaultEntryValue(NT_Handle pubsubentry, const Value& value);
+
+ TimestampedBoolean GetAtomicBoolean(NT_Handle subentry, bool defaultValue);
+ TimestampedInteger GetAtomicInteger(NT_Handle subentry, int64_t defaultValue);
+ TimestampedFloat GetAtomicFloat(NT_Handle subentry, float defaultValue);
+ TimestampedDouble GetAtomicDouble(NT_Handle subentry, double defaultValue);
+ TimestampedString GetAtomicString(NT_Handle subentry,
+ std::string_view defaultValue);
+ TimestampedRaw GetAtomicRaw(NT_Handle subentry,
+ std::span<const uint8_t> defaultValue);
+ TimestampedBooleanArray GetAtomicBooleanArray(
+ NT_Handle subentry, std::span<const int> defaultValue);
+ TimestampedIntegerArray GetAtomicIntegerArray(
+ NT_Handle subentry, std::span<const int64_t> defaultValue);
+ TimestampedFloatArray GetAtomicFloatArray(
+ NT_Handle subentry, std::span<const float> defaultValue);
+ TimestampedDoubleArray GetAtomicDoubleArray(
+ NT_Handle subentry, std::span<const double> defaultValue);
+ TimestampedStringArray GetAtomicStringArray(
+ NT_Handle subentry, std::span<const std::string> defaultValue);
+
+ TimestampedStringView GetAtomicString(NT_Handle subentry,
+ wpi::SmallVectorImpl<char>& buf,
+ std::string_view defaultValue);
+ TimestampedRawView GetAtomicRaw(NT_Handle subentry,
+ wpi::SmallVectorImpl<uint8_t>& buf,
+ std::span<const uint8_t> defaultValue);
+ TimestampedBooleanArrayView GetAtomicBooleanArray(
+ NT_Handle subentry, wpi::SmallVectorImpl<int>& buf,
+ std::span<const int> defaultValue);
+ TimestampedIntegerArrayView GetAtomicIntegerArray(
+ NT_Handle subentry, wpi::SmallVectorImpl<int64_t>& buf,
+ std::span<const int64_t> defaultValue);
+ TimestampedFloatArrayView GetAtomicFloatArray(
+ NT_Handle subentry, wpi::SmallVectorImpl<float>& buf,
+ std::span<const float> defaultValue);
+ TimestampedDoubleArrayView GetAtomicDoubleArray(
+ NT_Handle subentry, wpi::SmallVectorImpl<double>& buf,
+ std::span<const double> defaultValue);
+
+ std::vector<Value> ReadQueueValue(NT_Handle subentry);
+
+ std::vector<TimestampedBoolean> ReadQueueBoolean(NT_Handle subentry);
+ std::vector<TimestampedInteger> ReadQueueInteger(NT_Handle subentry);
+ std::vector<TimestampedFloat> ReadQueueFloat(NT_Handle subentry);
+ std::vector<TimestampedDouble> ReadQueueDouble(NT_Handle subentry);
+ std::vector<TimestampedString> ReadQueueString(NT_Handle subentry);
+ std::vector<TimestampedRaw> ReadQueueRaw(NT_Handle subentry);
+ std::vector<TimestampedBooleanArray> ReadQueueBooleanArray(
+ NT_Handle subentry);
+ std::vector<TimestampedIntegerArray> ReadQueueIntegerArray(
+ NT_Handle subentry);
+ std::vector<TimestampedFloatArray> ReadQueueFloatArray(NT_Handle subentry);
+ std::vector<TimestampedDoubleArray> ReadQueueDoubleArray(NT_Handle subentry);
+ std::vector<TimestampedStringArray> ReadQueueStringArray(NT_Handle subentry);
+
+ //
+ // Backwards compatible user functions
+ //
+
+ Value GetEntryValue(NT_Handle subentry);
+ void SetEntryFlags(NT_Entry entry, unsigned int flags);
+ unsigned int GetEntryFlags(NT_Entry entry);
+
+ // Index-only
+ NT_Entry GetEntry(std::string_view name);
+
+ std::string GetEntryName(NT_Entry entry);
+ NT_Type GetEntryType(NT_Entry entry);
+ int64_t GetEntryLastChange(NT_Entry entry);
+
+ //
+ // Listener functions
+ //
+
+ void AddListener(NT_Listener listener,
+ std::span<const std::string_view> prefixes,
+ unsigned int mask);
+ void AddListener(NT_Listener listener, NT_Handle handle, unsigned int mask);
+
+ void RemoveListener(NT_Listener listener, unsigned int mask);
+
+ //
+ // Data log functions
+ //
+ NT_DataLogger StartDataLog(wpi::log::DataLog& log, std::string_view prefix,
+ std::string_view logPrefix);
+ void StopDataLog(NT_DataLogger logger);
+
+ void Reset();
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> m_impl;
+
+ wpi::mutex m_mutex;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/Log.h b/ntcore/src/main/native/cpp/Log.h
index d3066d7..7e052f9 100644
--- a/ntcore/src/main/native/cpp/Log.h
+++ b/ntcore/src/main/native/cpp/Log.h
@@ -2,22 +2,27 @@
// 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.
-#ifndef NTCORE_LOG_H_
-#define NTCORE_LOG_H_
+#pragma once
#include <wpi/Logger.h>
-#define LOG(level, format, ...) WPI_LOG(m_logger, level, format, __VA_ARGS__)
+#define LOG(level, format, ...) \
+ WPI_LOG(m_logger, level, format __VA_OPT__(, ) __VA_ARGS__)
#undef ERROR
-#define ERROR(format, ...) WPI_ERROR(m_logger, format, __VA_ARGS__)
-#define WARNING(format, ...) WPI_WARNING(m_logger, format, __VA_ARGS__)
-#define INFO(format, ...) WPI_INFO(m_logger, format, __VA_ARGS__)
+#define ERROR(format, ...) \
+ WPI_ERROR(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
+#define WARNING(format, ...) \
+ WPI_WARNING(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
+#define INFO(format, ...) WPI_INFO(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
-#define DEBUG0(format, ...) WPI_DEBUG(m_logger, format, __VA_ARGS__)
-#define DEBUG1(format, ...) WPI_DEBUG1(m_logger, format, __VA_ARGS__)
-#define DEBUG2(format, ...) WPI_DEBUG2(m_logger, format, __VA_ARGS__)
-#define DEBUG3(format, ...) WPI_DEBUG3(m_logger, format, __VA_ARGS__)
-#define DEBUG4(format, ...) WPI_DEBUG4(m_logger, format, __VA_ARGS__)
-
-#endif // NTCORE_LOG_H_
+#define DEBUG0(format, ...) \
+ WPI_DEBUG(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
+#define DEBUG1(format, ...) \
+ WPI_DEBUG1(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
+#define DEBUG2(format, ...) \
+ WPI_DEBUG2(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
+#define DEBUG3(format, ...) \
+ WPI_DEBUG3(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
+#define DEBUG4(format, ...) \
+ WPI_DEBUG4(m_logger, format __VA_OPT__(, ) __VA_ARGS__)
diff --git a/ntcore/src/main/native/cpp/LoggerImpl.cpp b/ntcore/src/main/native/cpp/LoggerImpl.cpp
index fcb59d6..f5742d5 100644
--- a/ntcore/src/main/native/cpp/LoggerImpl.cpp
+++ b/ntcore/src/main/native/cpp/LoggerImpl.cpp
@@ -5,23 +5,27 @@
#include "LoggerImpl.h"
#include <fmt/format.h>
+#include <wpi/Logger.h>
+#include <wpi/SmallVector.h>
#include <wpi/fs.h>
+#include "IListenerStorage.h"
+
using namespace nt;
static void DefaultLogger(unsigned int level, const char* file,
unsigned int line, const char* msg) {
- if (level == 20) {
+ if (level == wpi::WPI_LOG_INFO) {
fmt::print(stderr, "NT: {}\n", msg);
return;
}
std::string_view levelmsg;
- if (level >= 50) {
+ if (level >= wpi::WPI_LOG_CRITICAL) {
levelmsg = "CRITICAL";
- } else if (level >= 40) {
+ } else if (level >= wpi::WPI_LOG_ERROR) {
levelmsg = "ERROR";
- } else if (level >= 30) {
+ } else if (level >= wpi::WPI_LOG_WARNING) {
levelmsg = "WARNING";
} else {
return;
@@ -29,34 +33,107 @@
fmt::print(stderr, "NT: {}: {} ({}:{})\n", levelmsg, msg, file, line);
}
-LoggerImpl::LoggerImpl(int inst) : m_inst(inst) {}
+static constexpr unsigned int kFlagCritical = 1u << 16;
+static constexpr unsigned int kFlagError = 1u << 17;
+static constexpr unsigned int kFlagWarning = 1u << 18;
+static constexpr unsigned int kFlagInfo = 1u << 19;
+static constexpr unsigned int kFlagDebug = 1u << 20;
+static constexpr unsigned int kFlagDebug1 = 1u << 21;
+static constexpr unsigned int kFlagDebug2 = 1u << 22;
+static constexpr unsigned int kFlagDebug3 = 1u << 23;
+static constexpr unsigned int kFlagDebug4 = 1u << 24;
-void LoggerImpl::Start() {
- DoStart(m_inst);
+static unsigned int LevelToFlag(unsigned int level) {
+ if (level >= wpi::WPI_LOG_CRITICAL) {
+ return EventFlags::kLogMessage | kFlagCritical;
+ } else if (level >= wpi::WPI_LOG_ERROR) {
+ return EventFlags::kLogMessage | kFlagError;
+ } else if (level >= wpi::WPI_LOG_WARNING) {
+ return EventFlags::kLogMessage | kFlagWarning;
+ } else if (level >= wpi::WPI_LOG_INFO) {
+ return EventFlags::kLogMessage | kFlagInfo;
+ } else if (level >= wpi::WPI_LOG_DEBUG) {
+ return EventFlags::kLogMessage | kFlagDebug;
+ } else if (level >= wpi::WPI_LOG_DEBUG1) {
+ return EventFlags::kLogMessage | kFlagDebug1;
+ } else if (level >= wpi::WPI_LOG_DEBUG2) {
+ return EventFlags::kLogMessage | kFlagDebug2;
+ } else if (level >= wpi::WPI_LOG_DEBUG3) {
+ return EventFlags::kLogMessage | kFlagDebug3;
+ } else if (level >= wpi::WPI_LOG_DEBUG4) {
+ return EventFlags::kLogMessage | kFlagDebug4;
+ } else {
+ return EventFlags::kLogMessage;
+ }
}
-unsigned int LoggerImpl::Add(
- std::function<void(const LogMessage& msg)> callback, unsigned int min_level,
- unsigned int max_level) {
- return DoAdd(callback, min_level, max_level);
+static unsigned int LevelsToEventMask(unsigned int minLevel,
+ unsigned int maxLevel) {
+ unsigned int mask = 0;
+ if (minLevel <= wpi::WPI_LOG_CRITICAL && maxLevel >= wpi::WPI_LOG_CRITICAL) {
+ mask |= kFlagCritical;
+ }
+ if (minLevel <= wpi::WPI_LOG_ERROR && maxLevel >= wpi::WPI_LOG_ERROR) {
+ mask |= kFlagError;
+ }
+ if (minLevel <= wpi::WPI_LOG_WARNING && maxLevel >= wpi::WPI_LOG_WARNING) {
+ mask |= kFlagWarning;
+ }
+ if (minLevel <= wpi::WPI_LOG_INFO && maxLevel >= wpi::WPI_LOG_INFO) {
+ mask |= kFlagInfo;
+ }
+ if (minLevel <= wpi::WPI_LOG_DEBUG && maxLevel >= wpi::WPI_LOG_DEBUG) {
+ mask |= kFlagDebug;
+ }
+ if (minLevel <= wpi::WPI_LOG_DEBUG1 && maxLevel >= wpi::WPI_LOG_DEBUG1) {
+ mask |= kFlagDebug1;
+ }
+ if (minLevel <= wpi::WPI_LOG_DEBUG2 && maxLevel >= wpi::WPI_LOG_DEBUG2) {
+ mask |= kFlagDebug2;
+ }
+ if (minLevel <= wpi::WPI_LOG_DEBUG3 && maxLevel >= wpi::WPI_LOG_DEBUG3) {
+ mask |= kFlagDebug3;
+ }
+ if (minLevel <= wpi::WPI_LOG_DEBUG4 && maxLevel >= wpi::WPI_LOG_DEBUG4) {
+ mask |= kFlagDebug4;
+ }
+ if (mask == 0) {
+ mask = EventFlags::kLogMessage;
+ }
+ return mask;
}
-unsigned int LoggerImpl::AddPolled(unsigned int poller_uid,
- unsigned int min_level,
- unsigned int max_level) {
- return DoAdd(poller_uid, min_level, max_level);
+LoggerImpl::LoggerImpl(IListenerStorage& listenerStorage)
+ : m_listenerStorage{listenerStorage} {}
+
+LoggerImpl::~LoggerImpl() = default;
+
+void LoggerImpl::AddListener(NT_Listener listener, unsigned int minLevel,
+ unsigned int maxLevel) {
+ ++m_listenerCount;
+ std::scoped_lock lock{m_mutex};
+ m_listenerLevels.emplace_back(listener, minLevel, maxLevel);
+ m_listenerStorage.Activate(listener, LevelsToEventMask(minLevel, maxLevel),
+ [](unsigned int mask, Event* event) {
+ event->flags = NT_EVENT_LOGMESSAGE;
+ return true;
+ });
+}
+
+void LoggerImpl::RemoveListener(NT_Listener listener) {
+ --m_listenerCount;
+ std::scoped_lock lock{m_mutex};
+ std::erase_if(m_listenerLevels,
+ [&](auto& v) { return v.listener == listener; });
}
unsigned int LoggerImpl::GetMinLevel() {
- auto thr = GetThread();
- if (!thr) {
- return NT_LOG_INFO;
- }
+ // return 0;
+ std::scoped_lock lock{m_mutex};
unsigned int level = NT_LOG_INFO;
- for (size_t i = 0; i < thr->m_listeners.size(); ++i) {
- const auto& listener = thr->m_listeners[i];
- if (listener && listener.min_level < level) {
- level = listener.min_level;
+ for (auto&& listenerLevel : m_listenerLevels) {
+ if (listenerLevel.minLevel < level) {
+ level = listenerLevel.minLevel;
}
}
return level;
@@ -65,11 +142,10 @@
void LoggerImpl::Log(unsigned int level, const char* file, unsigned int line,
const char* msg) {
auto filename = fs::path{file}.filename();
- {
- auto thr = GetThread();
- if (!thr || thr->m_listeners.empty()) {
- DefaultLogger(level, filename.string().c_str(), line, msg);
- }
+ if (m_listenerCount == 0) {
+ DefaultLogger(level, filename.string().c_str(), line, msg);
+ } else {
+ m_listenerStorage.Notify(LevelToFlag(level), level, filename.string(), line,
+ msg);
}
- Send(UINT_MAX, 0, level, filename.string(), line, msg);
}
diff --git a/ntcore/src/main/native/cpp/LoggerImpl.h b/ntcore/src/main/native/cpp/LoggerImpl.h
index 2b577c1..13a1b80 100644
--- a/ntcore/src/main/native/cpp/LoggerImpl.h
+++ b/ntcore/src/main/native/cpp/LoggerImpl.h
@@ -2,76 +2,31 @@
// 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.
-#ifndef NTCORE_LOGGERIMPL_H_
-#define NTCORE_LOGGERIMPL_H_
+#pragma once
-#include <utility>
+#include <atomic>
+#include <vector>
-#include <wpi/CallbackManager.h>
+#include <wpi/mutex.h>
-#include "Handle.h"
+#include "IListenerStorage.h"
+#include "ntcore_c.h"
#include "ntcore_cpp.h"
namespace nt {
-namespace impl {
+class IListenerStorage;
-struct LoggerListenerData : public wpi::CallbackListenerData<
- std::function<void(const LogMessage& msg)>> {
- LoggerListenerData() = default;
- LoggerListenerData(std::function<void(const LogMessage& msg)> callback_,
- unsigned int min_level_, unsigned int max_level_)
- : CallbackListenerData(callback_),
- min_level(min_level_),
- max_level(max_level_) {}
- LoggerListenerData(unsigned int poller_uid_, unsigned int min_level_,
- unsigned int max_level_)
- : CallbackListenerData(poller_uid_),
- min_level(min_level_),
- max_level(max_level_) {}
-
- unsigned int min_level;
- unsigned int max_level;
-};
-
-class LoggerThread
- : public wpi::CallbackThread<LoggerThread, LogMessage, LoggerListenerData> {
+class LoggerImpl {
public:
- LoggerThread(std::function<void()> on_start, std::function<void()> on_exit,
- int inst)
- : CallbackThread(std::move(on_start), std::move(on_exit)), m_inst(inst) {}
+ explicit LoggerImpl(IListenerStorage& listenerStorage);
+ LoggerImpl(const LoggerImpl&) = delete;
+ LoggerImpl& operator=(const LoggerImpl&) = delete;
+ ~LoggerImpl();
- bool Matches(const LoggerListenerData& listener, const LogMessage& data) {
- return data.level >= listener.min_level && data.level <= listener.max_level;
- }
-
- void SetListener(LogMessage* data, unsigned int listener_uid) {
- data->logger = Handle(m_inst, listener_uid, Handle::kLogger).handle();
- }
-
- void DoCallback(std::function<void(const LogMessage& msg)> callback,
- const LogMessage& data) {
- callback(data);
- }
-
- int m_inst;
-};
-
-} // namespace impl
-
-class LoggerImpl : public wpi::CallbackManager<LoggerImpl, impl::LoggerThread> {
- friend class LoggerTest;
- friend class wpi::CallbackManager<LoggerImpl, impl::LoggerThread>;
-
- public:
- explicit LoggerImpl(int inst);
-
- void Start();
-
- unsigned int Add(std::function<void(const LogMessage& msg)> callback,
- unsigned int min_level, unsigned int max_level);
- unsigned int AddPolled(unsigned int poller_uid, unsigned int min_level,
- unsigned int max_level);
+ void AddListener(NT_Listener listener, unsigned int minLevel,
+ unsigned int maxLevel);
+ void RemoveListener(NT_Listener listener);
unsigned int GetMinLevel();
@@ -79,9 +34,20 @@
const char* msg);
private:
- int m_inst;
+ IListenerStorage& m_listenerStorage;
+ std::atomic_int m_listenerCount{0};
+ wpi::mutex m_mutex;
+
+ struct ListenerLevels {
+ ListenerLevels(NT_Listener listener, unsigned int minLevel,
+ unsigned int maxLevel)
+ : listener{listener}, minLevel{minLevel}, maxLevel{maxLevel} {}
+
+ NT_Listener listener;
+ unsigned int minLevel;
+ unsigned int maxLevel;
+ };
+ std::vector<ListenerLevels> m_listenerLevels;
};
} // namespace nt
-
-#endif // NTCORE_LOGGERIMPL_H_
diff --git a/ntcore/src/main/native/cpp/Message.cpp b/ntcore/src/main/native/cpp/Message.cpp
deleted file mode 100644
index 61efed1..0000000
--- a/ntcore/src/main/native/cpp/Message.cpp
+++ /dev/null
@@ -1,374 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "Message.h"
-
-#include <stdint.h>
-
-#include "Log.h"
-#include "WireDecoder.h"
-#include "WireEncoder.h"
-
-#define kClearAllMagic 0xD06CB27Aul
-
-using namespace nt;
-
-std::shared_ptr<Message> Message::Read(WireDecoder& decoder,
- GetEntryTypeFunc get_entry_type) {
- unsigned int msg_type = 0;
- if (!decoder.Read8(&msg_type)) {
- return nullptr;
- }
- auto msg =
- std::make_shared<Message>(static_cast<MsgType>(msg_type), private_init());
- switch (msg_type) {
- case kKeepAlive:
- break;
- case kClientHello: {
- unsigned int proto_rev;
- if (!decoder.Read16(&proto_rev)) {
- return nullptr;
- }
- msg->m_id = proto_rev;
- // This intentionally uses the provided proto_rev instead of
- // decoder.proto_rev().
- if (proto_rev >= 0x0300u) {
- if (!decoder.ReadString(&msg->m_str)) {
- return nullptr;
- }
- }
- break;
- }
- case kProtoUnsup: {
- if (!decoder.Read16(&msg->m_id)) {
- return nullptr; // proto rev
- }
- break;
- }
- case kServerHelloDone:
- break;
- case kServerHello:
- if (decoder.proto_rev() < 0x0300u) {
- decoder.set_error("received SERVER_HELLO in protocol < 3.0");
- return nullptr;
- }
- if (!decoder.Read8(&msg->m_flags)) {
- return nullptr;
- }
- if (!decoder.ReadString(&msg->m_str)) {
- return nullptr;
- }
- break;
- case kClientHelloDone:
- if (decoder.proto_rev() < 0x0300u) {
- decoder.set_error("received CLIENT_HELLO_DONE in protocol < 3.0");
- return nullptr;
- }
- break;
- case kEntryAssign: {
- if (!decoder.ReadString(&msg->m_str)) {
- return nullptr; // name
- }
- NT_Type type;
- if (!decoder.ReadType(&type)) {
- return nullptr; // entry type
- }
- if (!decoder.Read16(&msg->m_id)) {
- return nullptr; // id
- }
- if (!decoder.Read16(&msg->m_seq_num_uid)) {
- return nullptr; // seq num
- }
- if (decoder.proto_rev() >= 0x0300u) {
- if (!decoder.Read8(&msg->m_flags)) {
- return nullptr; // flags
- }
- }
- msg->m_value = decoder.ReadValue(type);
- if (!msg->m_value) {
- return nullptr;
- }
- break;
- }
- case kEntryUpdate: {
- if (!decoder.Read16(&msg->m_id)) {
- return nullptr; // id
- }
- if (!decoder.Read16(&msg->m_seq_num_uid)) {
- return nullptr; // seq num
- }
- NT_Type type;
- if (decoder.proto_rev() >= 0x0300u) {
- if (!decoder.ReadType(&type)) {
- return nullptr;
- }
- } else {
- type = get_entry_type(msg->m_id);
- }
- WPI_DEBUG4(decoder.logger(), "update message data type: {}", type);
- msg->m_value = decoder.ReadValue(type);
- if (!msg->m_value) {
- return nullptr;
- }
- break;
- }
- case kFlagsUpdate: {
- if (decoder.proto_rev() < 0x0300u) {
- decoder.set_error("received FLAGS_UPDATE in protocol < 3.0");
- return nullptr;
- }
- if (!decoder.Read16(&msg->m_id)) {
- return nullptr;
- }
- if (!decoder.Read8(&msg->m_flags)) {
- return nullptr;
- }
- break;
- }
- case kEntryDelete: {
- if (decoder.proto_rev() < 0x0300u) {
- decoder.set_error("received ENTRY_DELETE in protocol < 3.0");
- return nullptr;
- }
- if (!decoder.Read16(&msg->m_id)) {
- return nullptr;
- }
- break;
- }
- case kClearEntries: {
- if (decoder.proto_rev() < 0x0300u) {
- decoder.set_error("received CLEAR_ENTRIES in protocol < 3.0");
- return nullptr;
- }
- uint32_t magic;
- if (!decoder.Read32(&magic)) {
- return nullptr;
- }
- if (magic != kClearAllMagic) {
- decoder.set_error(
- "received incorrect CLEAR_ENTRIES magic value, ignoring");
- return nullptr;
- }
- break;
- }
- case kExecuteRpc: {
- if (decoder.proto_rev() < 0x0300u) {
- decoder.set_error("received EXECUTE_RPC in protocol < 3.0");
- return nullptr;
- }
- if (!decoder.Read16(&msg->m_id)) {
- return nullptr;
- }
- if (!decoder.Read16(&msg->m_seq_num_uid)) {
- return nullptr; // uid
- }
- uint64_t size;
- if (!decoder.ReadUleb128(&size)) {
- return nullptr;
- }
- const char* params;
- if (!decoder.Read(¶ms, size)) {
- return nullptr;
- }
- msg->m_str.assign(params, size);
- break;
- }
- case kRpcResponse: {
- if (decoder.proto_rev() < 0x0300u) {
- decoder.set_error("received RPC_RESPONSE in protocol < 3.0");
- return nullptr;
- }
- if (!decoder.Read16(&msg->m_id)) {
- return nullptr;
- }
- if (!decoder.Read16(&msg->m_seq_num_uid)) {
- return nullptr; // uid
- }
- uint64_t size;
- if (!decoder.ReadUleb128(&size)) {
- return nullptr;
- }
- const char* results;
- if (!decoder.Read(&results, size)) {
- return nullptr;
- }
- msg->m_str.assign(results, size);
- break;
- }
- default:
- decoder.set_error("unrecognized message type");
- WPI_INFO(decoder.logger(), "unrecognized message type: {}", msg_type);
- return nullptr;
- }
- return msg;
-}
-
-std::shared_ptr<Message> Message::ClientHello(std::string_view self_id) {
- auto msg = std::make_shared<Message>(kClientHello, private_init());
- msg->m_str = self_id;
- return msg;
-}
-
-std::shared_ptr<Message> Message::ServerHello(unsigned int flags,
- std::string_view self_id) {
- auto msg = std::make_shared<Message>(kServerHello, private_init());
- msg->m_str = self_id;
- msg->m_flags = flags;
- return msg;
-}
-
-std::shared_ptr<Message> Message::EntryAssign(std::string_view name,
- unsigned int id,
- unsigned int seq_num,
- std::shared_ptr<Value> value,
- unsigned int flags) {
- auto msg = std::make_shared<Message>(kEntryAssign, private_init());
- msg->m_str = name;
- msg->m_value = value;
- msg->m_id = id;
- msg->m_flags = flags;
- msg->m_seq_num_uid = seq_num;
- return msg;
-}
-
-std::shared_ptr<Message> Message::EntryUpdate(unsigned int id,
- unsigned int seq_num,
- std::shared_ptr<Value> value) {
- auto msg = std::make_shared<Message>(kEntryUpdate, private_init());
- msg->m_value = value;
- msg->m_id = id;
- msg->m_seq_num_uid = seq_num;
- return msg;
-}
-
-std::shared_ptr<Message> Message::FlagsUpdate(unsigned int id,
- unsigned int flags) {
- auto msg = std::make_shared<Message>(kFlagsUpdate, private_init());
- msg->m_id = id;
- msg->m_flags = flags;
- return msg;
-}
-
-std::shared_ptr<Message> Message::EntryDelete(unsigned int id) {
- auto msg = std::make_shared<Message>(kEntryDelete, private_init());
- msg->m_id = id;
- return msg;
-}
-
-std::shared_ptr<Message> Message::ExecuteRpc(unsigned int id, unsigned int uid,
- std::string_view params) {
- auto msg = std::make_shared<Message>(kExecuteRpc, private_init());
- msg->m_str = params;
- msg->m_id = id;
- msg->m_seq_num_uid = uid;
- return msg;
-}
-
-std::shared_ptr<Message> Message::RpcResponse(unsigned int id, unsigned int uid,
- std::string_view result) {
- auto msg = std::make_shared<Message>(kRpcResponse, private_init());
- msg->m_str = result;
- msg->m_id = id;
- msg->m_seq_num_uid = uid;
- return msg;
-}
-
-void Message::Write(WireEncoder& encoder) const {
- switch (m_type) {
- case kKeepAlive:
- encoder.Write8(kKeepAlive);
- break;
- case kClientHello:
- encoder.Write8(kClientHello);
- encoder.Write16(encoder.proto_rev());
- if (encoder.proto_rev() < 0x0300u) {
- return;
- }
- encoder.WriteString(m_str);
- break;
- case kProtoUnsup:
- encoder.Write8(kProtoUnsup);
- encoder.Write16(encoder.proto_rev());
- break;
- case kServerHelloDone:
- encoder.Write8(kServerHelloDone);
- break;
- case kServerHello:
- if (encoder.proto_rev() < 0x0300u) {
- return; // new message in version 3.0
- }
- encoder.Write8(kServerHello);
- encoder.Write8(m_flags);
- encoder.WriteString(m_str);
- break;
- case kClientHelloDone:
- if (encoder.proto_rev() < 0x0300u) {
- return; // new message in version 3.0
- }
- encoder.Write8(kClientHelloDone);
- break;
- case kEntryAssign:
- encoder.Write8(kEntryAssign);
- encoder.WriteString(m_str);
- encoder.WriteType(m_value->type());
- encoder.Write16(m_id);
- encoder.Write16(m_seq_num_uid);
- if (encoder.proto_rev() >= 0x0300u) {
- encoder.Write8(m_flags);
- }
- encoder.WriteValue(*m_value);
- break;
- case kEntryUpdate:
- encoder.Write8(kEntryUpdate);
- encoder.Write16(m_id);
- encoder.Write16(m_seq_num_uid);
- if (encoder.proto_rev() >= 0x0300u) {
- encoder.WriteType(m_value->type());
- }
- encoder.WriteValue(*m_value);
- break;
- case kFlagsUpdate:
- if (encoder.proto_rev() < 0x0300u) {
- return; // new message in version 3.0
- }
- encoder.Write8(kFlagsUpdate);
- encoder.Write16(m_id);
- encoder.Write8(m_flags);
- break;
- case kEntryDelete:
- if (encoder.proto_rev() < 0x0300u) {
- return; // new message in version 3.0
- }
- encoder.Write8(kEntryDelete);
- encoder.Write16(m_id);
- break;
- case kClearEntries:
- if (encoder.proto_rev() < 0x0300u) {
- return; // new message in version 3.0
- }
- encoder.Write8(kClearEntries);
- encoder.Write32(kClearAllMagic);
- break;
- case kExecuteRpc:
- if (encoder.proto_rev() < 0x0300u) {
- return; // new message in version 3.0
- }
- encoder.Write8(kExecuteRpc);
- encoder.Write16(m_id);
- encoder.Write16(m_seq_num_uid);
- encoder.WriteString(m_str);
- break;
- case kRpcResponse:
- if (encoder.proto_rev() < 0x0300u) {
- return; // new message in version 3.0
- }
- encoder.Write8(kRpcResponse);
- encoder.Write16(m_id);
- encoder.Write16(m_seq_num_uid);
- encoder.WriteString(m_str);
- break;
- default:
- break;
- }
-}
diff --git a/ntcore/src/main/native/cpp/Message.h b/ntcore/src/main/native/cpp/Message.h
deleted file mode 100644
index ec34a75..0000000
--- a/ntcore/src/main/native/cpp/Message.h
+++ /dev/null
@@ -1,114 +0,0 @@
-// 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.
-
-#ifndef NTCORE_MESSAGE_H_
-#define NTCORE_MESSAGE_H_
-
-#include <functional>
-#include <memory>
-#include <string>
-#include <string_view>
-
-#include "networktables/NetworkTableValue.h"
-
-namespace nt {
-
-class WireDecoder;
-class WireEncoder;
-
-class Message {
- struct private_init {};
-
- public:
- enum MsgType {
- kUnknown = -1,
- kKeepAlive = 0x00,
- kClientHello = 0x01,
- kProtoUnsup = 0x02,
- kServerHelloDone = 0x03,
- kServerHello = 0x04,
- kClientHelloDone = 0x05,
- kEntryAssign = 0x10,
- kEntryUpdate = 0x11,
- kFlagsUpdate = 0x12,
- kEntryDelete = 0x13,
- kClearEntries = 0x14,
- kExecuteRpc = 0x20,
- kRpcResponse = 0x21
- };
- using GetEntryTypeFunc = std::function<NT_Type(unsigned int id)>;
-
- Message() = default;
- Message(MsgType type, const private_init&) : m_type(type) {}
-
- MsgType type() const { return m_type; }
- bool Is(MsgType type) const { return type == m_type; }
-
- // Message data accessors. Callers are responsible for knowing what data is
- // actually provided for a particular message.
- std::string_view str() const { return m_str; }
- std::shared_ptr<Value> value() const { return m_value; }
- unsigned int id() const { return m_id; }
- unsigned int flags() const { return m_flags; }
- unsigned int seq_num_uid() const { return m_seq_num_uid; }
-
- // Read and write from wire representation
- void Write(WireEncoder& encoder) const;
- static std::shared_ptr<Message> Read(WireDecoder& decoder,
- GetEntryTypeFunc get_entry_type);
-
- // Create messages without data
- static std::shared_ptr<Message> KeepAlive() {
- return std::make_shared<Message>(kKeepAlive, private_init());
- }
- static std::shared_ptr<Message> ProtoUnsup() {
- return std::make_shared<Message>(kProtoUnsup, private_init());
- }
- static std::shared_ptr<Message> ServerHelloDone() {
- return std::make_shared<Message>(kServerHelloDone, private_init());
- }
- static std::shared_ptr<Message> ClientHelloDone() {
- return std::make_shared<Message>(kClientHelloDone, private_init());
- }
- static std::shared_ptr<Message> ClearEntries() {
- return std::make_shared<Message>(kClearEntries, private_init());
- }
-
- // Create messages with data
- static std::shared_ptr<Message> ClientHello(std::string_view self_id);
- static std::shared_ptr<Message> ServerHello(unsigned int flags,
- std::string_view self_id);
- static std::shared_ptr<Message> EntryAssign(std::string_view name,
- unsigned int id,
- unsigned int seq_num,
- std::shared_ptr<Value> value,
- unsigned int flags);
- static std::shared_ptr<Message> EntryUpdate(unsigned int id,
- unsigned int seq_num,
- std::shared_ptr<Value> value);
- static std::shared_ptr<Message> FlagsUpdate(unsigned int id,
- unsigned int flags);
- static std::shared_ptr<Message> EntryDelete(unsigned int id);
- static std::shared_ptr<Message> ExecuteRpc(unsigned int id, unsigned int uid,
- std::string_view params);
- static std::shared_ptr<Message> RpcResponse(unsigned int id, unsigned int uid,
- std::string_view result);
-
- Message(const Message&) = delete;
- Message& operator=(const Message&) = delete;
-
- private:
- MsgType m_type{kUnknown};
-
- // Message data. Use varies by message type.
- std::string m_str;
- std::shared_ptr<Value> m_value;
- unsigned int m_id{0}; // also used for proto_rev
- unsigned int m_flags{0};
- unsigned int m_seq_num_uid{0};
-};
-
-} // namespace nt
-
-#endif // NTCORE_MESSAGE_H_
diff --git a/ntcore/src/main/native/cpp/NetworkClient.cpp b/ntcore/src/main/native/cpp/NetworkClient.cpp
new file mode 100644
index 0000000..a329fb9
--- /dev/null
+++ b/ntcore/src/main/native/cpp/NetworkClient.cpp
@@ -0,0 +1,557 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "NetworkClient.h"
+
+#include <stdint.h>
+
+#include <atomic>
+#include <memory>
+#include <vector>
+
+#include <fmt/format.h>
+#include <wpi/SmallString.h>
+#include <wpi/StringExtras.h>
+#include <wpinet/DsClient.h>
+#include <wpinet/EventLoopRunner.h>
+#include <wpinet/HttpUtil.h>
+#include <wpinet/ParallelTcpConnector.h>
+#include <wpinet/WebSocket.h>
+#include <wpinet/uv/Async.h>
+#include <wpinet/uv/Loop.h>
+#include <wpinet/uv/Tcp.h>
+#include <wpinet/uv/Timer.h>
+#include <wpinet/uv/util.h>
+
+#include "IConnectionList.h"
+#include "Log.h"
+#include "net/ClientImpl.h"
+#include "net/Message.h"
+#include "net/NetworkLoopQueue.h"
+#include "net/WebSocketConnection.h"
+#include "net3/ClientImpl3.h"
+#include "net3/UvStreamConnection3.h"
+
+using namespace nt;
+namespace uv = wpi::uv;
+
+static constexpr uv::Timer::Time kReconnectRate{1000};
+static constexpr uv::Timer::Time kWebsocketHandshakeTimeout{500};
+// use a larger max message size for websockets
+static constexpr size_t kMaxMessageSize = 2 * 1024 * 1024;
+
+namespace {
+
+class NCImpl {
+ public:
+ NCImpl(int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger);
+ virtual ~NCImpl() = default;
+
+ // user-facing functions
+ void SetServers(std::span<const std::pair<std::string, unsigned int>> servers,
+ unsigned int defaultPort);
+ void StartDSClient(unsigned int port);
+ void StopDSClient();
+
+ virtual void TcpConnected(uv::Tcp& tcp) = 0;
+ virtual void Disconnect(std::string_view reason);
+
+ // invariants
+ int m_inst;
+ net::ILocalStorage& m_localStorage;
+ IConnectionList& m_connList;
+ wpi::Logger& m_logger;
+ std::string m_id;
+
+ // used only from loop
+ std::shared_ptr<wpi::ParallelTcpConnector> m_parallelConnect;
+ std::shared_ptr<uv::Timer> m_readLocalTimer;
+ std::shared_ptr<uv::Timer> m_sendValuesTimer;
+ std::shared_ptr<uv::Async<>> m_flushLocal;
+ std::shared_ptr<uv::Async<>> m_flush;
+
+ std::vector<net::ClientMessage> m_localMsgs;
+
+ std::vector<std::pair<std::string, unsigned int>> m_servers;
+
+ std::pair<std::string, unsigned int> m_dsClientServer{"", 0};
+ std::shared_ptr<wpi::DsClient> m_dsClient;
+
+ // shared with user
+ std::atomic<uv::Async<>*> m_flushLocalAtomic{nullptr};
+ std::atomic<uv::Async<>*> m_flushAtomic{nullptr};
+
+ net::NetworkLoopQueue m_localQueue;
+
+ int m_connHandle = 0;
+
+ wpi::EventLoopRunner m_loopRunner;
+ uv::Loop& m_loop;
+};
+
+class NCImpl3 : public NCImpl {
+ public:
+ NCImpl3(int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger);
+ ~NCImpl3() override;
+
+ void HandleLocal();
+ void TcpConnected(uv::Tcp& tcp) final;
+ void Disconnect(std::string_view reason) override;
+
+ std::shared_ptr<net3::UvStreamConnection3> m_wire;
+ std::shared_ptr<net3::ClientImpl3> m_clientImpl;
+};
+
+class NCImpl4 : public NCImpl {
+ public:
+ NCImpl4(
+ int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated);
+ ~NCImpl4() override;
+
+ void HandleLocal();
+ void TcpConnected(uv::Tcp& tcp) final;
+ void WsConnected(wpi::WebSocket& ws, uv::Tcp& tcp);
+ void Disconnect(std::string_view reason) override;
+
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ m_timeSyncUpdated;
+ std::shared_ptr<net::WebSocketConnection> m_wire;
+ std::unique_ptr<net::ClientImpl> m_clientImpl;
+};
+
+} // namespace
+
+NCImpl::NCImpl(int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger)
+ : m_inst{inst},
+ m_localStorage{localStorage},
+ m_connList{connList},
+ m_logger{logger},
+ m_id{id},
+ m_localQueue{logger},
+ m_loop{*m_loopRunner.GetLoop()} {
+ m_localMsgs.reserve(net::NetworkLoopQueue::kInitialQueueSize);
+
+ INFO("starting network client");
+}
+
+void NCImpl::SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers,
+ unsigned int defaultPort) {
+ std::vector<std::pair<std::string, unsigned int>> serversCopy;
+ serversCopy.reserve(servers.size());
+ for (auto&& server : servers) {
+ serversCopy.emplace_back(wpi::trim(server.first),
+ server.second == 0 ? defaultPort : server.second);
+ }
+
+ m_loopRunner.ExecAsync(
+ [this, servers = std::move(serversCopy)](uv::Loop&) mutable {
+ m_servers = std::move(servers);
+ if (m_dsClientServer.first.empty()) {
+ m_parallelConnect->SetServers(m_servers);
+ }
+ });
+}
+
+void NCImpl::StartDSClient(unsigned int port) {
+ m_loopRunner.ExecAsync([=, this](uv::Loop& loop) {
+ if (m_dsClient) {
+ return;
+ }
+ m_dsClientServer.second = port == 0 ? NT_DEFAULT_PORT4 : port;
+ m_dsClient = wpi::DsClient::Create(m_loop, m_logger);
+ m_dsClient->setIp.connect([this](std::string_view ip) {
+ m_dsClientServer.first = ip;
+ m_parallelConnect->SetServers({{m_dsClientServer}});
+ });
+ m_dsClient->clearIp.connect([this] {
+ m_dsClientServer.first.clear();
+ m_parallelConnect->SetServers(m_servers);
+ });
+ });
+}
+
+void NCImpl::StopDSClient() {
+ m_loopRunner.ExecAsync([this](uv::Loop& loop) {
+ if (m_dsClient) {
+ m_dsClient->Close();
+ m_dsClient.reset();
+ }
+ });
+}
+
+void NCImpl::Disconnect(std::string_view reason) {
+ if (m_readLocalTimer) {
+ m_readLocalTimer->Stop();
+ }
+ m_sendValuesTimer->Stop();
+ m_localStorage.ClearNetwork();
+ m_localQueue.ClearQueue();
+ m_connList.RemoveConnection(m_connHandle);
+ m_connHandle = 0;
+
+ // start trying to connect again
+ uv::Timer::SingleShot(m_loop, kReconnectRate,
+ [this] { m_parallelConnect->Disconnected(); });
+}
+
+NCImpl3::NCImpl3(int inst, std::string_view id,
+ net::ILocalStorage& localStorage, IConnectionList& connList,
+ wpi::Logger& logger)
+ : NCImpl{inst, id, localStorage, connList, logger} {
+ m_loopRunner.ExecAsync([this](uv::Loop& loop) {
+ m_parallelConnect = wpi::ParallelTcpConnector::Create(
+ loop, kReconnectRate, m_logger,
+ [this](uv::Tcp& tcp) { TcpConnected(tcp); });
+
+ m_sendValuesTimer = uv::Timer::Create(loop);
+ m_sendValuesTimer->timeout.connect([this] {
+ if (m_clientImpl) {
+ HandleLocal();
+ m_clientImpl->SendPeriodic(m_loop.Now().count());
+ }
+ });
+
+ // set up flush async
+ m_flush = uv::Async<>::Create(m_loop);
+ m_flush->wakeup.connect([this] {
+ if (m_clientImpl) {
+ HandleLocal();
+ m_clientImpl->SendPeriodic(m_loop.Now().count());
+ }
+ });
+ m_flushAtomic = m_flush.get();
+
+ m_flushLocal = uv::Async<>::Create(m_loop);
+ m_flushLocal->wakeup.connect([this] { HandleLocal(); });
+ m_flushLocalAtomic = m_flushLocal.get();
+ });
+}
+
+NCImpl3::~NCImpl3() {
+ // must explicitly destroy these on loop
+ m_loopRunner.ExecSync([&](auto&) {
+ m_clientImpl.reset();
+ m_wire.reset();
+ });
+ // shut down loop here to avoid race
+ m_loopRunner.Stop();
+}
+
+void NCImpl3::HandleLocal() {
+ m_localQueue.ReadQueue(&m_localMsgs);
+ if (m_clientImpl) {
+ m_clientImpl->HandleLocal(m_localMsgs);
+ }
+}
+
+void NCImpl3::TcpConnected(uv::Tcp& tcp) {
+ tcp.SetNoDelay(true);
+
+ // create as shared_ptr and capture in lambda because there may be multiple
+ // simultaneous attempts
+ auto wire = std::make_shared<net3::UvStreamConnection3>(tcp);
+ auto clientImpl = std::make_shared<net3::ClientImpl3>(
+ m_loop.Now().count(), m_inst, *wire, m_logger, [this](uint32_t repeatMs) {
+ DEBUG4("Setting periodic timer to {}", repeatMs);
+ m_sendValuesTimer->Start(uv::Timer::Time{repeatMs},
+ uv::Timer::Time{repeatMs});
+ });
+ clientImpl->Start(
+ m_id, [this, wire,
+ clientWeak = std::weak_ptr<net3::ClientImpl3>{clientImpl}, &tcp] {
+ auto clientImpl = clientWeak.lock();
+ if (!clientImpl) {
+ return;
+ }
+ if (m_connList.IsConnected()) {
+ tcp.Close(); // no longer needed
+ return;
+ }
+
+ m_parallelConnect->Succeeded(tcp);
+
+ m_wire = std::move(wire);
+ m_clientImpl = std::move(clientImpl);
+
+ ConnectionInfo connInfo;
+ uv::AddrToName(tcp.GetPeer(), &connInfo.remote_ip,
+ &connInfo.remote_port);
+ connInfo.protocol_version = 0x0300;
+
+ INFO("CONNECTED NT3 to {} port {}", connInfo.remote_ip,
+ connInfo.remote_port);
+ m_connHandle = m_connList.AddConnection(connInfo);
+
+ tcp.error.connect([this, &tcp](uv::Error err) {
+ DEBUG3("NT3 TCP error {}", err.str());
+ if (!tcp.IsLoopClosing()) {
+ Disconnect(err.str());
+ }
+ });
+ tcp.end.connect([this, &tcp] {
+ DEBUG3("NT3 TCP read ended");
+ if (!tcp.IsLoopClosing()) {
+ Disconnect("remote end closed connection");
+ }
+ });
+ tcp.closed.connect([this, &tcp] {
+ DEBUG3("NT3 TCP connection closed");
+ if (!tcp.IsLoopClosing()) {
+ Disconnect(m_wire->GetDisconnectReason());
+ }
+ });
+
+ m_clientImpl->SetLocal(&m_localStorage);
+ m_localStorage.StartNetwork(&m_localQueue);
+ HandleLocal();
+ });
+
+ tcp.SetData(clientImpl);
+ tcp.data.connect(
+ [clientImpl = clientImpl.get()](uv::Buffer& buf, size_t len) {
+ clientImpl->ProcessIncoming(
+ {reinterpret_cast<const uint8_t*>(buf.base), len});
+ });
+ tcp.StartRead();
+}
+
+void NCImpl3::Disconnect(std::string_view reason) {
+ INFO("DISCONNECTED NT3 connection: {}", reason);
+ m_clientImpl.reset();
+ m_wire.reset();
+ NCImpl::Disconnect(reason);
+}
+
+NCImpl4::NCImpl4(
+ int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated)
+ : NCImpl{inst, id, localStorage, connList, logger},
+ m_timeSyncUpdated{std::move(timeSyncUpdated)} {
+ m_loopRunner.ExecAsync([this](uv::Loop& loop) {
+ m_parallelConnect = wpi::ParallelTcpConnector::Create(
+ loop, kReconnectRate, m_logger,
+ [this](uv::Tcp& tcp) { TcpConnected(tcp); });
+
+ m_readLocalTimer = uv::Timer::Create(loop);
+ m_readLocalTimer->timeout.connect([this] {
+ if (m_clientImpl) {
+ HandleLocal();
+ m_clientImpl->SendControl(m_loop.Now().count());
+ }
+ });
+ m_readLocalTimer->Start(uv::Timer::Time{100}, uv::Timer::Time{100});
+
+ m_sendValuesTimer = uv::Timer::Create(loop);
+ m_sendValuesTimer->timeout.connect([this] {
+ if (m_clientImpl) {
+ HandleLocal();
+ m_clientImpl->SendValues(m_loop.Now().count());
+ }
+ });
+
+ // set up flush async
+ m_flush = uv::Async<>::Create(m_loop);
+ m_flush->wakeup.connect([this] {
+ if (m_clientImpl) {
+ HandleLocal();
+ m_clientImpl->SendValues(m_loop.Now().count());
+ }
+ });
+ m_flushAtomic = m_flush.get();
+
+ m_flushLocal = uv::Async<>::Create(m_loop);
+ m_flushLocal->wakeup.connect([this] { HandleLocal(); });
+ m_flushLocalAtomic = m_flushLocal.get();
+ });
+}
+
+NCImpl4::~NCImpl4() {
+ // must explicitly destroy these on loop
+ m_loopRunner.ExecSync([&](auto&) {
+ m_clientImpl.reset();
+ m_wire.reset();
+ });
+ // shut down loop here to avoid race
+ m_loopRunner.Stop();
+}
+
+void NCImpl4::HandleLocal() {
+ m_localQueue.ReadQueue(&m_localMsgs);
+ if (m_clientImpl) {
+ m_clientImpl->HandleLocal(std::move(m_localMsgs));
+ }
+}
+
+void NCImpl4::TcpConnected(uv::Tcp& tcp) {
+ tcp.SetNoDelay(true);
+ // Start the WS client
+ if (m_logger.min_level() >= wpi::WPI_LOG_DEBUG4) {
+ std::string ip;
+ unsigned int port = 0;
+ uv::AddrToName(tcp.GetPeer(), &ip, &port);
+ DEBUG4("Starting WebSocket client on {} port {}", ip, port);
+ }
+ wpi::WebSocket::ClientOptions options;
+ options.handshakeTimeout = kWebsocketHandshakeTimeout;
+ wpi::SmallString<128> idBuf;
+ auto ws = wpi::WebSocket::CreateClient(
+ tcp, fmt::format("/nt/{}", wpi::EscapeURI(m_id, idBuf)), "",
+ {{"networktables.first.wpi.edu"}}, options);
+ ws->SetMaxMessageSize(kMaxMessageSize);
+ ws->open.connect([this, &tcp, ws = ws.get()](std::string_view) {
+ if (m_connList.IsConnected()) {
+ ws->Terminate(1006, "no longer needed");
+ return;
+ }
+ WsConnected(*ws, tcp);
+ });
+}
+
+void NCImpl4::WsConnected(wpi::WebSocket& ws, uv::Tcp& tcp) {
+ m_parallelConnect->Succeeded(tcp);
+
+ ConnectionInfo connInfo;
+ uv::AddrToName(tcp.GetPeer(), &connInfo.remote_ip, &connInfo.remote_port);
+ connInfo.protocol_version = 0x0400;
+
+ INFO("CONNECTED NT4 to {} port {}", connInfo.remote_ip, connInfo.remote_port);
+ m_connHandle = m_connList.AddConnection(connInfo);
+
+ m_wire = std::make_shared<net::WebSocketConnection>(ws);
+ m_clientImpl = std::make_unique<net::ClientImpl>(
+ m_loop.Now().count(), m_inst, *m_wire, m_logger, m_timeSyncUpdated,
+ [this](uint32_t repeatMs) {
+ DEBUG4("Setting periodic timer to {}", repeatMs);
+ m_sendValuesTimer->Start(uv::Timer::Time{repeatMs},
+ uv::Timer::Time{repeatMs});
+ });
+ m_clientImpl->SetLocal(&m_localStorage);
+ m_localStorage.StartNetwork(&m_localQueue);
+ HandleLocal();
+ m_clientImpl->SendInitial();
+ ws.closed.connect([this, &ws](uint16_t, std::string_view reason) {
+ if (!ws.GetStream().IsLoopClosing()) {
+ Disconnect(reason);
+ }
+ });
+ ws.text.connect([this](std::string_view data, bool) {
+ if (m_clientImpl) {
+ m_clientImpl->ProcessIncomingText(data);
+ }
+ });
+ ws.binary.connect([this](std::span<const uint8_t> data, bool) {
+ if (m_clientImpl) {
+ m_clientImpl->ProcessIncomingBinary(data);
+ }
+ });
+}
+
+void NCImpl4::Disconnect(std::string_view reason) {
+ INFO("DISCONNECTED NT4 connection: {}", reason);
+ m_clientImpl.reset();
+ m_wire.reset();
+ NCImpl::Disconnect(reason);
+ m_timeSyncUpdated(0, 0, false);
+}
+
+class NetworkClient::Impl final : public NCImpl4 {
+ public:
+ Impl(int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated)
+ : NCImpl4{inst, id, localStorage,
+ connList, logger, std::move(timeSyncUpdated)} {}
+};
+
+NetworkClient::NetworkClient(
+ int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated)
+ : m_impl{std::make_unique<Impl>(inst, id, localStorage, connList, logger,
+ std::move(timeSyncUpdated))} {}
+
+NetworkClient::~NetworkClient() {
+ m_impl->m_localStorage.ClearNetwork();
+ m_impl->m_connList.ClearConnections();
+}
+
+void NetworkClient::SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers) {
+ m_impl->SetServers(servers, NT_DEFAULT_PORT4);
+}
+
+void NetworkClient::StartDSClient(unsigned int port) {
+ m_impl->StartDSClient(port);
+}
+
+void NetworkClient::StopDSClient() {
+ m_impl->StopDSClient();
+}
+
+void NetworkClient::FlushLocal() {
+ m_impl->m_loopRunner.ExecAsync([this](uv::Loop&) { m_impl->HandleLocal(); });
+}
+
+void NetworkClient::Flush() {
+ m_impl->m_loopRunner.ExecAsync([this](uv::Loop&) {
+ m_impl->HandleLocal();
+ if (m_impl->m_clientImpl) {
+ m_impl->m_clientImpl->SendValues(m_impl->m_loop.Now().count());
+ }
+ });
+}
+
+class NetworkClient3::Impl final : public NCImpl3 {
+ public:
+ Impl(int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger)
+ : NCImpl3{inst, id, localStorage, connList, logger} {}
+};
+
+NetworkClient3::NetworkClient3(int inst, std::string_view id,
+ net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger)
+ : m_impl{std::make_unique<Impl>(inst, id, localStorage, connList, logger)} {
+}
+
+NetworkClient3::~NetworkClient3() {
+ m_impl->m_localStorage.ClearNetwork();
+ m_impl->m_connList.ClearConnections();
+}
+
+void NetworkClient3::SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers) {
+ m_impl->SetServers(servers, NT_DEFAULT_PORT3);
+}
+
+void NetworkClient3::StartDSClient(unsigned int port) {
+ m_impl->StartDSClient(port);
+}
+
+void NetworkClient3::StopDSClient() {
+ m_impl->StopDSClient();
+}
+
+void NetworkClient3::FlushLocal() {
+ if (auto async = m_impl->m_flushLocalAtomic.load(std::memory_order_relaxed)) {
+ async->UnsafeSend();
+ }
+}
+
+void NetworkClient3::Flush() {
+ if (auto async = m_impl->m_flushAtomic.load(std::memory_order_relaxed)) {
+ async->UnsafeSend();
+ }
+}
diff --git a/ntcore/src/main/native/cpp/NetworkClient.h b/ntcore/src/main/native/cpp/NetworkClient.h
new file mode 100644
index 0000000..34bf379
--- /dev/null
+++ b/ntcore/src/main/native/cpp/NetworkClient.h
@@ -0,0 +1,74 @@
+// 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 <functional>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include "INetworkClient.h"
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class Logger;
+} // namespace wpi
+
+namespace nt::net {
+class ILocalStorage;
+} // namespace nt::net
+
+namespace nt {
+
+class IConnectionList;
+
+class NetworkClient final : public INetworkClient {
+ public:
+ NetworkClient(
+ int inst, std::string_view id, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated);
+ ~NetworkClient() final;
+
+ void SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers) final;
+
+ void StartDSClient(unsigned int port) final;
+ void StopDSClient() final;
+
+ void FlushLocal() final;
+ void Flush() final;
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> m_impl;
+};
+
+class NetworkClient3 final : public INetworkClient {
+ public:
+ NetworkClient3(int inst, std::string_view id,
+ net::ILocalStorage& localStorage, IConnectionList& connList,
+ wpi::Logger& logger);
+ ~NetworkClient3() final;
+
+ void SetServers(
+ std::span<const std::pair<std::string, unsigned int>> servers) final;
+
+ void StartDSClient(unsigned int port) final;
+ void StopDSClient() final;
+
+ void FlushLocal() final;
+ void Flush() final;
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> m_impl;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/NetworkConnection.cpp b/ntcore/src/main/native/cpp/NetworkConnection.cpp
deleted file mode 100644
index 838eecb..0000000
--- a/ntcore/src/main/native/cpp/NetworkConnection.cpp
+++ /dev/null
@@ -1,378 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "NetworkConnection.h"
-
-#include <utility>
-
-#include <wpi/NetworkStream.h>
-#include <wpi/raw_socket_istream.h>
-#include <wpi/timestamp.h>
-
-#include "IConnectionNotifier.h"
-#include "Log.h"
-#include "WireDecoder.h"
-#include "WireEncoder.h"
-
-using namespace nt;
-
-NetworkConnection::NetworkConnection(unsigned int uid,
- std::unique_ptr<wpi::NetworkStream> stream,
- IConnectionNotifier& notifier,
- wpi::Logger& logger,
- HandshakeFunc handshake,
- Message::GetEntryTypeFunc get_entry_type)
- : m_uid(uid),
- m_stream(std::move(stream)),
- m_notifier(notifier),
- m_logger(logger),
- m_handshake(std::move(handshake)),
- m_get_entry_type(std::move(get_entry_type)),
- m_state(kCreated) {
- m_active = false;
- m_proto_rev = 0x0300;
- m_last_update = 0;
-
- // turn off Nagle algorithm; we bundle packets for transmission
- m_stream->setNoDelay();
-}
-
-NetworkConnection::~NetworkConnection() {
- Stop();
-}
-
-void NetworkConnection::Start() {
- if (m_active) {
- return;
- }
- m_active = true;
- set_state(kInit);
- // clear queue
- while (!m_outgoing.empty()) {
- m_outgoing.pop();
- }
- // reset shutdown flags
- {
- std::scoped_lock lock(m_shutdown_mutex);
- m_read_shutdown = false;
- m_write_shutdown = false;
- }
- // start threads
- m_write_thread = std::thread(&NetworkConnection::WriteThreadMain, this);
- m_read_thread = std::thread(&NetworkConnection::ReadThreadMain, this);
-}
-
-void NetworkConnection::Stop() {
- DEBUG2("NetworkConnection stopping ({})", fmt::ptr(this));
- set_state(kDead);
- m_active = false;
- // closing the stream so the read thread terminates
- if (m_stream) {
- m_stream->close();
- }
- // send an empty outgoing message set so the write thread terminates
- m_outgoing.push(Outgoing());
- // wait for threads to terminate, with timeout
- if (m_write_thread.joinable()) {
- std::unique_lock lock(m_shutdown_mutex);
- auto timeout_time =
- std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
- if (m_write_shutdown_cv.wait_until(lock, timeout_time,
- [&] { return m_write_shutdown; })) {
- m_write_thread.join();
- } else {
- m_write_thread.detach(); // timed out, detach it
- }
- }
- if (m_read_thread.joinable()) {
- std::unique_lock lock(m_shutdown_mutex);
- auto timeout_time =
- std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
- if (m_read_shutdown_cv.wait_until(lock, timeout_time,
- [&] { return m_read_shutdown; })) {
- m_read_thread.join();
- } else {
- m_read_thread.detach(); // timed out, detach it
- }
- }
- // clear queue
- while (!m_outgoing.empty()) {
- m_outgoing.pop();
- }
-}
-
-ConnectionInfo NetworkConnection::info() const {
- return ConnectionInfo{remote_id(), std::string{m_stream->getPeerIP()},
- static_cast<unsigned int>(m_stream->getPeerPort()),
- m_last_update, m_proto_rev};
-}
-
-unsigned int NetworkConnection::proto_rev() const {
- return m_proto_rev;
-}
-
-void NetworkConnection::set_proto_rev(unsigned int proto_rev) {
- m_proto_rev = proto_rev;
-}
-
-NetworkConnection::State NetworkConnection::state() const {
- std::scoped_lock lock(m_state_mutex);
- return m_state;
-}
-
-void NetworkConnection::set_state(State state) {
- std::scoped_lock lock(m_state_mutex);
- // Don't update state any more once we've died
- if (m_state == kDead) {
- return;
- }
- // One-shot notify state changes
- if (m_state != kActive && state == kActive) {
- m_notifier.NotifyConnection(true, info());
- }
- if (m_state != kDead && state == kDead) {
- m_notifier.NotifyConnection(false, info());
- }
- m_state = state;
-}
-
-std::string NetworkConnection::remote_id() const {
- std::scoped_lock lock(m_remote_id_mutex);
- return m_remote_id;
-}
-
-void NetworkConnection::set_remote_id(std::string_view remote_id) {
- std::scoped_lock lock(m_remote_id_mutex);
- m_remote_id = remote_id;
-}
-
-void NetworkConnection::ReadThreadMain() {
- wpi::raw_socket_istream is(*m_stream);
- WireDecoder decoder(is, m_proto_rev, m_logger);
-
- set_state(kHandshake);
- if (!m_handshake(
- *this,
- [&] {
- decoder.set_proto_rev(m_proto_rev);
- auto msg = Message::Read(decoder, m_get_entry_type);
- if (!msg && decoder.error()) {
- DEBUG0("error reading in handshake: {}", decoder.error());
- }
- return msg;
- },
- [&](auto msgs) {
- m_outgoing.emplace(std::vector<std::shared_ptr<Message>>(
- msgs.begin(), msgs.end()));
- })) {
- set_state(kDead);
- m_active = false;
- goto done;
- }
-
- set_state(kActive);
- while (m_active) {
- if (!m_stream) {
- break;
- }
- decoder.set_proto_rev(m_proto_rev);
- decoder.Reset();
- auto msg = Message::Read(decoder, m_get_entry_type);
- if (!msg) {
- if (decoder.error()) {
- INFO("read error: {}", decoder.error());
- }
- // terminate connection on bad message
- if (m_stream) {
- m_stream->close();
- }
- break;
- }
- DEBUG3("received type={} with str={} id={} seq_num={}", msg->type(),
- msg->str(), msg->id(), msg->seq_num_uid());
- m_last_update = Now();
- m_process_incoming(std::move(msg), this);
- }
- DEBUG2("read thread died ({})", fmt::ptr(this));
- set_state(kDead);
- m_active = false;
- m_outgoing.push(Outgoing()); // also kill write thread
-
-done:
- // use condition variable to signal thread shutdown
- {
- std::scoped_lock lock(m_shutdown_mutex);
- m_read_shutdown = true;
- m_read_shutdown_cv.notify_one();
- }
-}
-
-void NetworkConnection::WriteThreadMain() {
- WireEncoder encoder(m_proto_rev);
-
- while (m_active) {
- auto msgs = m_outgoing.pop();
- DEBUG4("{}", "write thread woke up");
- if (msgs.empty()) {
- continue;
- }
- encoder.set_proto_rev(m_proto_rev);
- encoder.Reset();
- DEBUG3("sending {} messages", msgs.size());
- for (auto& msg : msgs) {
- if (msg) {
- DEBUG3("sending type={} with str={} id={} seq_num={}", msg->type(),
- msg->str(), msg->id(), msg->seq_num_uid());
- msg->Write(encoder);
- }
- }
- wpi::NetworkStream::Error err;
- if (!m_stream) {
- break;
- }
- if (encoder.size() == 0) {
- continue;
- }
- if (m_stream->send(encoder.data(), encoder.size(), &err) == 0) {
- break;
- }
- DEBUG4("sent {} bytes", encoder.size());
- }
- DEBUG2("write thread died ({})", fmt::ptr(this));
- set_state(kDead);
- m_active = false;
- if (m_stream) {
- m_stream->close(); // also kill read thread
- }
-
- // use condition variable to signal thread shutdown
- {
- std::scoped_lock lock(m_shutdown_mutex);
- m_write_shutdown = true;
- m_write_shutdown_cv.notify_one();
- }
-}
-
-void NetworkConnection::QueueOutgoing(std::shared_ptr<Message> msg) {
- std::scoped_lock lock(m_pending_mutex);
-
- // Merge with previous. One case we don't combine: delete/assign loop.
- switch (msg->type()) {
- case Message::kEntryAssign:
- case Message::kEntryUpdate: {
- // don't do this for unassigned id's
- unsigned int id = msg->id();
- if (id == 0xffff) {
- m_pending_outgoing.push_back(msg);
- break;
- }
- if (id < m_pending_update.size() && m_pending_update[id].first != 0) {
- // overwrite the previous one for this id
- auto& oldmsg = m_pending_outgoing[m_pending_update[id].first - 1];
- if (oldmsg && oldmsg->Is(Message::kEntryAssign) &&
- msg->Is(Message::kEntryUpdate)) {
- // need to update assignment with new seq_num and value
- oldmsg = Message::EntryAssign(oldmsg->str(), id, msg->seq_num_uid(),
- msg->value(), oldmsg->flags());
- } else {
- oldmsg = msg; // easy update
- }
- } else {
- // new, but remember it
- size_t pos = m_pending_outgoing.size();
- m_pending_outgoing.push_back(msg);
- if (id >= m_pending_update.size()) {
- m_pending_update.resize(id + 1);
- }
- m_pending_update[id].first = pos + 1;
- }
- break;
- }
- case Message::kEntryDelete: {
- // don't do this for unassigned id's
- unsigned int id = msg->id();
- if (id == 0xffff) {
- m_pending_outgoing.push_back(msg);
- break;
- }
-
- // clear previous updates
- if (id < m_pending_update.size()) {
- if (m_pending_update[id].first != 0) {
- m_pending_outgoing[m_pending_update[id].first - 1].reset();
- m_pending_update[id].first = 0;
- }
- if (m_pending_update[id].second != 0) {
- m_pending_outgoing[m_pending_update[id].second - 1].reset();
- m_pending_update[id].second = 0;
- }
- }
-
- // add deletion
- m_pending_outgoing.push_back(msg);
- break;
- }
- case Message::kFlagsUpdate: {
- // don't do this for unassigned id's
- unsigned int id = msg->id();
- if (id == 0xffff) {
- m_pending_outgoing.push_back(msg);
- break;
- }
- if (id < m_pending_update.size() && m_pending_update[id].second != 0) {
- // overwrite the previous one for this id
- m_pending_outgoing[m_pending_update[id].second - 1] = msg;
- } else {
- // new, but remember it
- size_t pos = m_pending_outgoing.size();
- m_pending_outgoing.push_back(msg);
- if (id >= m_pending_update.size()) {
- m_pending_update.resize(id + 1);
- }
- m_pending_update[id].second = pos + 1;
- }
- break;
- }
- case Message::kClearEntries: {
- // knock out all previous assigns/updates!
- for (auto& i : m_pending_outgoing) {
- if (!i) {
- continue;
- }
- auto t = i->type();
- if (t == Message::kEntryAssign || t == Message::kEntryUpdate ||
- t == Message::kFlagsUpdate || t == Message::kEntryDelete ||
- t == Message::kClearEntries) {
- i.reset();
- }
- }
- m_pending_update.resize(0);
- m_pending_outgoing.push_back(msg);
- break;
- }
- default:
- m_pending_outgoing.push_back(msg);
- break;
- }
-}
-
-void NetworkConnection::PostOutgoing(bool keep_alive) {
- std::scoped_lock lock(m_pending_mutex);
- auto now = std::chrono::steady_clock::now();
- if (m_pending_outgoing.empty()) {
- if (!keep_alive) {
- return;
- }
- // send keep-alives once a second (if no other messages have been sent)
- if ((now - m_last_post) < std::chrono::seconds(1)) {
- return;
- }
- m_outgoing.emplace(Outgoing{Message::KeepAlive()});
- } else {
- m_outgoing.emplace(std::move(m_pending_outgoing));
- m_pending_outgoing.resize(0);
- m_pending_update.resize(0);
- }
- m_last_post = now;
-} // NOLINT
diff --git a/ntcore/src/main/native/cpp/NetworkConnection.h b/ntcore/src/main/native/cpp/NetworkConnection.h
deleted file mode 100644
index 59f18ff..0000000
--- a/ntcore/src/main/native/cpp/NetworkConnection.h
+++ /dev/null
@@ -1,124 +0,0 @@
-// 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.
-
-#ifndef NTCORE_NETWORKCONNECTION_H_
-#define NTCORE_NETWORKCONNECTION_H_
-
-#include <stdint.h>
-
-#include <atomic>
-#include <chrono>
-#include <memory>
-#include <string>
-#include <string_view>
-#include <thread>
-#include <utility>
-#include <vector>
-
-#include <wpi/ConcurrentQueue.h>
-#include <wpi/condition_variable.h>
-#include <wpi/mutex.h>
-#include <wpi/span.h>
-
-#include "INetworkConnection.h"
-#include "Message.h"
-#include "ntcore_cpp.h"
-
-namespace wpi {
-class Logger;
-class NetworkStream;
-} // namespace wpi
-
-namespace nt {
-
-class IConnectionNotifier;
-
-class NetworkConnection : public INetworkConnection {
- public:
- using HandshakeFunc = std::function<bool(
- NetworkConnection& conn,
- std::function<std::shared_ptr<Message>()> get_msg,
- std::function<void(wpi::span<std::shared_ptr<Message>>)> send_msgs)>;
- using ProcessIncomingFunc =
- std::function<void(std::shared_ptr<Message>, NetworkConnection*)>;
- using Outgoing = std::vector<std::shared_ptr<Message>>;
- using OutgoingQueue = wpi::ConcurrentQueue<Outgoing>;
-
- NetworkConnection(unsigned int uid,
- std::unique_ptr<wpi::NetworkStream> stream,
- IConnectionNotifier& notifier, wpi::Logger& logger,
- HandshakeFunc handshake,
- Message::GetEntryTypeFunc get_entry_type);
- ~NetworkConnection() override;
-
- // Set the input processor function. This must be called before Start().
- void set_process_incoming(ProcessIncomingFunc func) {
- m_process_incoming = func;
- }
-
- void Start();
- void Stop();
-
- ConnectionInfo info() const final;
-
- bool active() const { return m_active; }
- wpi::NetworkStream& stream() { return *m_stream; }
-
- void QueueOutgoing(std::shared_ptr<Message> msg) final;
- void PostOutgoing(bool keep_alive) override;
-
- unsigned int uid() const { return m_uid; }
-
- unsigned int proto_rev() const final;
- void set_proto_rev(unsigned int proto_rev) final;
-
- State state() const final;
- void set_state(State state) final;
-
- std::string remote_id() const;
- void set_remote_id(std::string_view remote_id);
-
- uint64_t last_update() const { return m_last_update; }
-
- NetworkConnection(const NetworkConnection&) = delete;
- NetworkConnection& operator=(const NetworkConnection&) = delete;
-
- private:
- void ReadThreadMain();
- void WriteThreadMain();
-
- unsigned int m_uid;
- std::unique_ptr<wpi::NetworkStream> m_stream;
- IConnectionNotifier& m_notifier;
- wpi::Logger& m_logger;
- OutgoingQueue m_outgoing;
- HandshakeFunc m_handshake;
- Message::GetEntryTypeFunc m_get_entry_type;
- ProcessIncomingFunc m_process_incoming;
- std::thread m_read_thread;
- std::thread m_write_thread;
- std::atomic_bool m_active;
- std::atomic_uint m_proto_rev;
- mutable wpi::mutex m_state_mutex;
- State m_state;
- mutable wpi::mutex m_remote_id_mutex;
- std::string m_remote_id;
- std::atomic_ullong m_last_update;
- std::chrono::steady_clock::time_point m_last_post;
-
- wpi::mutex m_pending_mutex;
- Outgoing m_pending_outgoing;
- std::vector<std::pair<size_t, size_t>> m_pending_update;
-
- // Condition variables for shutdown
- wpi::mutex m_shutdown_mutex;
- wpi::condition_variable m_read_shutdown_cv;
- wpi::condition_variable m_write_shutdown_cv;
- bool m_read_shutdown = false;
- bool m_write_shutdown = false;
-};
-
-} // namespace nt
-
-#endif // NTCORE_NETWORKCONNECTION_H_
diff --git a/ntcore/src/main/native/cpp/NetworkServer.cpp b/ntcore/src/main/native/cpp/NetworkServer.cpp
new file mode 100644
index 0000000..0086d24
--- /dev/null
+++ b/ntcore/src/main/native/cpp/NetworkServer.cpp
@@ -0,0 +1,571 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "NetworkServer.h"
+
+#include <stdint.h>
+
+#include <atomic>
+#include <span>
+#include <system_error>
+#include <vector>
+
+#include <wpi/SmallString.h>
+#include <wpi/StringExtras.h>
+#include <wpi/fs.h>
+#include <wpi/mutex.h>
+#include <wpi/raw_istream.h>
+#include <wpi/raw_ostream.h>
+#include <wpinet/EventLoopRunner.h>
+#include <wpinet/HttpUtil.h>
+#include <wpinet/HttpWebSocketServerConnection.h>
+#include <wpinet/UrlParser.h>
+#include <wpinet/uv/Async.h>
+#include <wpinet/uv/Tcp.h>
+#include <wpinet/uv/Work.h>
+#include <wpinet/uv/util.h>
+
+#include "IConnectionList.h"
+#include "InstanceImpl.h"
+#include "Log.h"
+#include "net/Message.h"
+#include "net/NetworkLoopQueue.h"
+#include "net/ServerImpl.h"
+#include "net/WebSocketConnection.h"
+#include "net3/UvStreamConnection3.h"
+
+using namespace nt;
+namespace uv = wpi::uv;
+
+// use a larger max message size for websockets
+static constexpr size_t kMaxMessageSize = 2 * 1024 * 1024;
+
+namespace {
+
+class NSImpl;
+
+class ServerConnection {
+ public:
+ ServerConnection(NSImpl& server, std::string_view addr, unsigned int port,
+ wpi::Logger& logger)
+ : m_server{server},
+ m_connInfo{fmt::format("{}:{}", addr, port)},
+ m_logger{logger} {
+ m_info.remote_ip = addr;
+ m_info.remote_port = port;
+ }
+
+ int GetClientId() const { return m_clientId; }
+
+ protected:
+ void SetupPeriodicTimer();
+ void UpdatePeriodicTimer(uint32_t repeatMs);
+ void ConnectionClosed();
+
+ NSImpl& m_server;
+ ConnectionInfo m_info;
+ std::string m_connInfo;
+ wpi::Logger& m_logger;
+ int m_clientId;
+
+ private:
+ std::shared_ptr<uv::Timer> m_sendValuesTimer;
+};
+
+class ServerConnection4 final
+ : public ServerConnection,
+ public wpi::HttpWebSocketServerConnection<ServerConnection4> {
+ public:
+ ServerConnection4(std::shared_ptr<uv::Stream> stream, NSImpl& server,
+ std::string_view addr, unsigned int port,
+ wpi::Logger& logger)
+ : ServerConnection{server, addr, port, logger},
+ HttpWebSocketServerConnection(stream, {"networktables.first.wpi.edu"}) {
+ m_info.protocol_version = 0x0400;
+ }
+
+ private:
+ void ProcessRequest() final;
+ void ProcessWsUpgrade() final;
+
+ std::shared_ptr<net::WebSocketConnection> m_wire;
+};
+
+class ServerConnection3 : public ServerConnection {
+ public:
+ ServerConnection3(std::shared_ptr<uv::Stream> stream, NSImpl& server,
+ std::string_view addr, unsigned int port,
+ wpi::Logger& logger);
+
+ private:
+ std::shared_ptr<net3::UvStreamConnection3> m_wire;
+};
+
+class NSImpl {
+ public:
+ NSImpl(std::string_view persistFilename, std::string_view listenAddress,
+ unsigned int port3, unsigned int port4,
+ net::ILocalStorage& localStorage, IConnectionList& connList,
+ wpi::Logger& logger, std::function<void()> initDone);
+ ~NSImpl();
+
+ void HandleLocal();
+ void LoadPersistent();
+ void SavePersistent(std::string_view filename, std::string_view data);
+ void Init();
+ void AddConnection(ServerConnection* conn, const ConnectionInfo& info);
+ void RemoveConnection(ServerConnection* conn);
+
+ net::ILocalStorage& m_localStorage;
+ IConnectionList& m_connList;
+ wpi::Logger& m_logger;
+ std::function<void()> m_initDone;
+ std::string m_persistentData;
+ std::string m_persistentFilename;
+ std::string m_listenAddress;
+ unsigned int m_port3;
+ unsigned int m_port4;
+
+ // used only from loop
+ std::shared_ptr<uv::Timer> m_readLocalTimer;
+ std::shared_ptr<uv::Timer> m_savePersistentTimer;
+ std::shared_ptr<uv::Async<>> m_flushLocal;
+ std::shared_ptr<uv::Async<>> m_flush;
+ bool m_shutdown = false;
+
+ std::vector<net::ClientMessage> m_localMsgs;
+
+ net::ServerImpl m_serverImpl;
+
+ // shared with user (must be atomic or mutex-protected)
+ std::atomic<uv::Async<>*> m_flushLocalAtomic{nullptr};
+ std::atomic<uv::Async<>*> m_flushAtomic{nullptr};
+ mutable wpi::mutex m_mutex;
+ struct Connection {
+ ServerConnection* conn;
+ int connHandle;
+ };
+ std::vector<Connection> m_connections;
+
+ net::NetworkLoopQueue m_localQueue;
+
+ wpi::EventLoopRunner m_loopRunner;
+ wpi::uv::Loop& m_loop;
+};
+
+} // namespace
+
+void ServerConnection::SetupPeriodicTimer() {
+ m_sendValuesTimer = uv::Timer::Create(m_server.m_loop);
+ m_sendValuesTimer->timeout.connect([this] {
+ m_server.HandleLocal();
+ m_server.m_serverImpl.SendValues(m_clientId, m_server.m_loop.Now().count());
+ });
+}
+
+void ServerConnection::UpdatePeriodicTimer(uint32_t repeatMs) {
+ if (repeatMs == UINT32_MAX) {
+ m_sendValuesTimer->Stop();
+ } else {
+ m_sendValuesTimer->Start(uv::Timer::Time{repeatMs},
+ uv::Timer::Time{repeatMs});
+ }
+}
+
+void ServerConnection::ConnectionClosed() {
+ // don't call back into m_server if it's being destroyed
+ if (!m_sendValuesTimer->IsLoopClosing()) {
+ m_server.m_serverImpl.RemoveClient(m_clientId);
+ m_server.RemoveConnection(this);
+ }
+ m_sendValuesTimer->Close();
+}
+
+void ServerConnection4::ProcessRequest() {
+ DEBUG1("HTTP request: '{}'", m_request.GetUrl());
+ wpi::UrlParser url{m_request.GetUrl(),
+ m_request.GetMethod() == wpi::HTTP_CONNECT};
+ if (!url.IsValid()) {
+ // failed to parse URL
+ SendError(400);
+ return;
+ }
+
+ std::string_view path;
+ if (url.HasPath()) {
+ path = url.GetPath();
+ }
+ DEBUG4("path: \"{}\"", path);
+
+ std::string_view query;
+ if (url.HasQuery()) {
+ query = url.GetQuery();
+ }
+ DEBUG4("query: \"{}\"\n", query);
+
+ const bool isGET = m_request.GetMethod() == wpi::HTTP_GET;
+ if (isGET && path == "/") {
+ // build HTML root page
+ SendResponse(200, "OK", "text/html",
+ "<html><head><title>NetworkTables</title></head>"
+ "<body><p>WebSockets must be used to access NetworkTables."
+ "</body></html>");
+ } else if (isGET && path == "/nt/persistent.json") {
+ SendResponse(200, "OK", "application/json",
+ m_server.m_serverImpl.DumpPersistent());
+ } else {
+ SendError(404, "Resource not found");
+ }
+}
+
+void ServerConnection4::ProcessWsUpgrade() {
+ // get name from URL
+ wpi::UrlParser url{m_request.GetUrl(), false};
+ std::string_view path;
+ if (url.HasPath()) {
+ path = url.GetPath();
+ }
+ DEBUG4("path: '{}'", path);
+
+ wpi::SmallString<128> nameBuf;
+ std::string_view name;
+ bool err = false;
+ if (wpi::starts_with(path, "/nt/")) {
+ name = wpi::UnescapeURI(wpi::drop_front(path, 4), nameBuf, &err);
+ }
+ if (err || name.empty()) {
+ INFO("invalid path '{}' (from {}), must match /nt/[clientId], closing",
+ path, m_connInfo);
+ m_websocket->Fail(
+ 404, fmt::format("invalid path '{}', must match /nt/[clientId]", path));
+ return;
+ }
+
+ m_websocket->SetMaxMessageSize(kMaxMessageSize);
+
+ m_websocket->open.connect([this, name = std::string{name}](std::string_view) {
+ m_wire = std::make_shared<net::WebSocketConnection>(*m_websocket);
+ // TODO: set local flag appropriately
+ std::string dedupName;
+ std::tie(dedupName, m_clientId) = m_server.m_serverImpl.AddClient(
+ name, m_connInfo, false, *m_wire,
+ [this](uint32_t repeatMs) { UpdatePeriodicTimer(repeatMs); });
+ INFO("CONNECTED NT4 client '{}' (from {})", dedupName, m_connInfo);
+ m_info.remote_id = dedupName;
+ m_server.AddConnection(this, m_info);
+ m_websocket->closed.connect([this](uint16_t, std::string_view reason) {
+ INFO("DISCONNECTED NT4 client '{}' (from {}): {}", m_info.remote_id,
+ m_connInfo, reason);
+ ConnectionClosed();
+ });
+ m_websocket->text.connect([this](std::string_view data, bool) {
+ m_server.m_serverImpl.ProcessIncomingText(m_clientId, data);
+ });
+ m_websocket->binary.connect([this](std::span<const uint8_t> data, bool) {
+ m_server.m_serverImpl.ProcessIncomingBinary(m_clientId, data);
+ });
+
+ SetupPeriodicTimer();
+ });
+}
+
+ServerConnection3::ServerConnection3(std::shared_ptr<uv::Stream> stream,
+ NSImpl& server, std::string_view addr,
+ unsigned int port, wpi::Logger& logger)
+ : ServerConnection{server, addr, port, logger},
+ m_wire{std::make_shared<net3::UvStreamConnection3>(*stream)} {
+ m_info.remote_ip = addr;
+ m_info.remote_port = port;
+
+ // TODO: set local flag appropriately
+ m_clientId = m_server.m_serverImpl.AddClient3(
+ m_connInfo, false, *m_wire,
+ [this](std::string_view name, uint16_t proto) {
+ m_info.remote_id = name;
+ m_info.protocol_version = proto;
+ m_server.AddConnection(this, m_info);
+ INFO("CONNECTED NT3 client '{}' (from {})", name, m_connInfo);
+ },
+ [this](uint32_t repeatMs) { UpdatePeriodicTimer(repeatMs); });
+
+ stream->error.connect([this](uv::Error err) {
+ if (!m_wire->GetDisconnectReason().empty()) {
+ return;
+ }
+ m_wire->Disconnect(fmt::format("stream error: {}", err.name()));
+ m_wire->GetStream().Shutdown([this] { m_wire->GetStream().Close(); });
+ });
+ stream->end.connect([this] {
+ if (!m_wire->GetDisconnectReason().empty()) {
+ return;
+ }
+ m_wire->Disconnect("remote end closed connection");
+ m_wire->GetStream().Shutdown([this] { m_wire->GetStream().Close(); });
+ });
+ stream->closed.connect([this] {
+ INFO("DISCONNECTED NT3 client '{}' (from {}): {}", m_info.remote_id,
+ m_connInfo, m_wire->GetDisconnectReason());
+ ConnectionClosed();
+ });
+ stream->data.connect([this](uv::Buffer& buf, size_t size) {
+ m_server.m_serverImpl.ProcessIncomingBinary(
+ m_clientId, {reinterpret_cast<const uint8_t*>(buf.base), size});
+ });
+ stream->StartRead();
+
+ SetupPeriodicTimer();
+}
+
+NSImpl::NSImpl(std::string_view persistentFilename,
+ std::string_view listenAddress, unsigned int port3,
+ unsigned int port4, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void()> initDone)
+ : m_localStorage{localStorage},
+ m_connList{connList},
+ m_logger{logger},
+ m_initDone{std::move(initDone)},
+ m_persistentFilename{persistentFilename},
+ m_listenAddress{wpi::trim(listenAddress)},
+ m_port3{port3},
+ m_port4{port4},
+ m_serverImpl{logger},
+ m_localQueue{logger},
+ m_loop(*m_loopRunner.GetLoop()) {
+ m_localMsgs.reserve(net::NetworkLoopQueue::kInitialQueueSize);
+ m_loopRunner.ExecAsync([=, this](uv::Loop& loop) {
+ // connect local storage to server
+ m_serverImpl.SetLocal(&m_localStorage);
+ m_localStorage.StartNetwork(&m_localQueue);
+ HandleLocal();
+
+ // load persistent file first, then initialize
+ uv::QueueWork(
+ m_loop, [this] { LoadPersistent(); }, [this] { Init(); });
+ });
+}
+
+NSImpl::~NSImpl() {
+ m_loopRunner.ExecAsync([this](uv::Loop&) { m_shutdown = true; });
+}
+
+void NSImpl::HandleLocal() {
+ m_localQueue.ReadQueue(&m_localMsgs);
+ m_serverImpl.HandleLocal(m_localMsgs);
+}
+
+void NSImpl::LoadPersistent() {
+ std::error_code ec;
+ auto size = fs::file_size(m_persistentFilename, ec);
+ wpi::raw_fd_istream is{m_persistentFilename, ec};
+ if (ec.value() != 0) {
+ INFO("could not open persistent file '{}': {}", m_persistentFilename,
+ ec.message());
+ return;
+ }
+ is.readinto(m_persistentData, size);
+ DEBUG4("read data: {}", m_persistentData);
+ if (is.has_error()) {
+ WARNING("error reading persistent file");
+ return;
+ }
+}
+
+void NSImpl::SavePersistent(std::string_view filename, std::string_view data) {
+ // write to temporary file
+ auto tmp = fmt::format("{}.tmp", filename);
+ std::error_code ec;
+ wpi::raw_fd_ostream os{tmp, ec, fs::F_Text};
+ if (ec.value() != 0) {
+ INFO("could not open persistent file '{}' for write: {}", tmp,
+ ec.message());
+ return;
+ }
+ os << data;
+ os.close();
+ if (os.has_error()) {
+ fs::remove(tmp);
+ return;
+ }
+
+ // move to real file
+ auto bak = fmt::format("{}.bck", filename);
+ fs::remove(bak, ec);
+ fs::rename(filename, bak, ec);
+ fs::rename(tmp, filename, ec);
+ if (ec.value() != 0) {
+ // attempt to restore backup
+ fs::rename(bak, filename, ec);
+ }
+}
+
+void NSImpl::Init() {
+ if (m_shutdown) {
+ return;
+ }
+ auto errs = m_serverImpl.LoadPersistent(m_persistentData);
+ if (!errs.empty()) {
+ WARNING("error reading persistent file: {}", errs);
+ }
+
+ // set up timers
+ m_readLocalTimer = uv::Timer::Create(m_loop);
+ m_readLocalTimer->timeout.connect([this] {
+ HandleLocal();
+ m_serverImpl.SendControl(m_loop.Now().count());
+ });
+ m_readLocalTimer->Start(uv::Timer::Time{100}, uv::Timer::Time{100});
+
+ m_savePersistentTimer = uv::Timer::Create(m_loop);
+ m_savePersistentTimer->timeout.connect([this] {
+ if (m_serverImpl.PersistentChanged()) {
+ uv::QueueWork(
+ m_loop,
+ [this, fn = m_persistentFilename,
+ data = m_serverImpl.DumpPersistent()] { SavePersistent(fn, data); },
+ nullptr);
+ }
+ });
+ m_savePersistentTimer->Start(uv::Timer::Time{1000}, uv::Timer::Time{1000});
+
+ // set up flush async
+ m_flush = uv::Async<>::Create(m_loop);
+ m_flush->wakeup.connect([this] {
+ HandleLocal();
+ for (auto&& conn : m_connections) {
+ m_serverImpl.SendValues(conn.conn->GetClientId(), m_loop.Now().count());
+ }
+ });
+ m_flushAtomic = m_flush.get();
+
+ m_flushLocal = uv::Async<>::Create(m_loop);
+ m_flushLocal->wakeup.connect([this] { HandleLocal(); });
+ m_flushLocalAtomic = m_flushLocal.get();
+
+ INFO("Listening on NT3 port {}, NT4 port {}", m_port3, m_port4);
+
+ if (m_port3 != 0) {
+ auto tcp3 = uv::Tcp::Create(m_loop);
+ tcp3->error.connect([logger = &m_logger](uv::Error err) {
+ WPI_INFO(*logger, "NT3 server socket error: {}", err.str());
+ });
+ tcp3->Bind(m_listenAddress, m_port3);
+
+ // when we get a NT3 connection, accept it and start reading
+ tcp3->connection.connect([this, srv = tcp3.get()] {
+ auto tcp = srv->Accept();
+ if (!tcp) {
+ return;
+ }
+ tcp->error.connect([logger = &m_logger](uv::Error err) {
+ WPI_INFO(*logger, "NT3 socket error: {}", err.str());
+ });
+ tcp->SetNoDelay(true);
+ std::string peerAddr;
+ unsigned int peerPort = 0;
+ if (uv::AddrToName(tcp->GetPeer(), &peerAddr, &peerPort) == 0) {
+ INFO("Got a NT3 connection from {} port {}", peerAddr, peerPort);
+ } else {
+ INFO("Got a NT3 connection from unknown");
+ }
+ auto conn = std::make_shared<ServerConnection3>(tcp, *this, peerAddr,
+ peerPort, m_logger);
+ tcp->SetData(conn);
+ });
+
+ tcp3->Listen();
+ }
+
+ if (m_port4 != 0) {
+ auto tcp4 = uv::Tcp::Create(m_loop);
+ tcp4->error.connect([logger = &m_logger](uv::Error err) {
+ WPI_INFO(*logger, "NT4 server socket error: {}", err.str());
+ });
+ tcp4->Bind(m_listenAddress, m_port4);
+
+ // when we get a NT4 connection, accept it and start reading
+ tcp4->connection.connect([this, srv = tcp4.get()] {
+ auto tcp = srv->Accept();
+ if (!tcp) {
+ return;
+ }
+ tcp->error.connect([logger = &m_logger](uv::Error err) {
+ WPI_INFO(*logger, "NT4 socket error: {}", err.str());
+ });
+ tcp->SetNoDelay(true);
+ std::string peerAddr;
+ unsigned int peerPort = 0;
+ if (uv::AddrToName(tcp->GetPeer(), &peerAddr, &peerPort) == 0) {
+ INFO("Got a NT4 connection from {} port {}", peerAddr, peerPort);
+ } else {
+ INFO("Got a NT4 connection from unknown");
+ }
+ auto conn = std::make_shared<ServerConnection4>(tcp, *this, peerAddr,
+ peerPort, m_logger);
+ tcp->SetData(conn);
+ });
+
+ tcp4->Listen();
+ }
+
+ if (m_initDone) {
+ DEBUG4("NetworkServer initDone()");
+ m_initDone();
+ m_initDone = nullptr;
+ }
+}
+
+void NSImpl::AddConnection(ServerConnection* conn, const ConnectionInfo& info) {
+ std::scoped_lock lock{m_mutex};
+ m_connections.emplace_back(Connection{conn, m_connList.AddConnection(info)});
+ m_serverImpl.ConnectionsChanged(m_connList.GetConnections());
+}
+
+void NSImpl::RemoveConnection(ServerConnection* conn) {
+ std::scoped_lock lock{m_mutex};
+ auto it = std::find_if(m_connections.begin(), m_connections.end(),
+ [=](auto&& c) { return c.conn == conn; });
+ if (it != m_connections.end()) {
+ m_connList.RemoveConnection(it->connHandle);
+ m_connections.erase(it);
+ m_serverImpl.ConnectionsChanged(m_connList.GetConnections());
+ }
+}
+
+class NetworkServer::Impl final : public NSImpl {
+ public:
+ Impl(std::string_view persistFilename, std::string_view listenAddress,
+ unsigned int port3, unsigned int port4, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void()> initDone)
+ : NSImpl{persistFilename, listenAddress, port3, port4,
+ localStorage, connList, logger, std::move(initDone)} {}
+};
+
+NetworkServer::NetworkServer(std::string_view persistFilename,
+ std::string_view listenAddress, unsigned int port3,
+ unsigned int port4,
+ net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void()> initDone)
+ : m_impl{std::make_unique<Impl>(persistFilename, listenAddress, port3,
+ port4, localStorage, connList, logger,
+ std::move(initDone))} {}
+
+NetworkServer::~NetworkServer() {
+ m_impl->m_localStorage.ClearNetwork();
+ m_impl->m_connList.ClearConnections();
+}
+
+void NetworkServer::FlushLocal() {
+ if (auto async = m_impl->m_flushLocalAtomic.load(std::memory_order_relaxed)) {
+ async->UnsafeSend();
+ }
+}
+
+void NetworkServer::Flush() {
+ if (auto async = m_impl->m_flushAtomic.load(std::memory_order_relaxed)) {
+ async->UnsafeSend();
+ }
+}
diff --git a/ntcore/src/main/native/cpp/NetworkServer.h b/ntcore/src/main/native/cpp/NetworkServer.h
new file mode 100644
index 0000000..b70c968
--- /dev/null
+++ b/ntcore/src/main/native/cpp/NetworkServer.h
@@ -0,0 +1,42 @@
+// 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 <functional>
+#include <memory>
+#include <string_view>
+
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class Logger;
+} // namespace wpi
+
+namespace nt::net {
+class ILocalStorage;
+} // namespace nt::net
+
+namespace nt {
+
+class IConnectionList;
+
+class NetworkServer {
+ public:
+ NetworkServer(std::string_view persistentFilename,
+ std::string_view listenAddress, unsigned int port3,
+ unsigned int port4, net::ILocalStorage& localStorage,
+ IConnectionList& connList, wpi::Logger& logger,
+ std::function<void()> initDone);
+ ~NetworkServer();
+
+ void FlushLocal();
+ void Flush();
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> m_impl;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/PubSubOptions.h b/ntcore/src/main/native/cpp/PubSubOptions.h
new file mode 100644
index 0000000..72ba2c3
--- /dev/null
+++ b/ntcore/src/main/native/cpp/PubSubOptions.h
@@ -0,0 +1,36 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+// internal helper class for PubSubOptions
+class PubSubOptionsImpl : public PubSubOptions {
+ public:
+ constexpr PubSubOptionsImpl() : PubSubOptionsImpl{kDefaultPubSubOptions} {}
+
+ /*implicit*/ constexpr PubSubOptionsImpl( // NOLINT
+ const PubSubOptions& options)
+ : PubSubOptions{options} {
+ if (periodic == 0) {
+ periodic = kDefaultPeriodic;
+ }
+ periodicMs = static_cast<unsigned int>(periodic * 1000);
+ if (pollStorage == 0) {
+ if (sendAll) {
+ pollStorage = 20;
+ } else {
+ pollStorage = 1;
+ }
+ }
+ }
+
+ static constexpr unsigned int kDefaultPeriodicMs = 100;
+ unsigned int periodicMs = kDefaultPeriodicMs;
+};
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/RpcServer.cpp b/ntcore/src/main/native/cpp/RpcServer.cpp
deleted file mode 100644
index b4bf96a..0000000
--- a/ntcore/src/main/native/cpp/RpcServer.cpp
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "RpcServer.h"
-
-using namespace nt;
-
-RpcServer::RpcServer(int inst, wpi::Logger& logger)
- : m_inst(inst), m_logger(logger) {}
-
-void RpcServer::Start() {
- DoStart(m_inst, m_logger);
-}
-
-unsigned int RpcServer::Add(
- std::function<void(const RpcAnswer& answer)> callback) {
- return DoAdd(callback);
-}
-
-unsigned int RpcServer::AddPolled(unsigned int poller_uid) {
- return DoAdd(poller_uid);
-}
-
-void RpcServer::RemoveRpc(unsigned int rpc_uid) {
- Remove(rpc_uid);
-}
-
-void RpcServer::ProcessRpc(unsigned int local_id, unsigned int call_uid,
- std::string_view name, std::string_view params,
- const ConnectionInfo& conn,
- SendResponseFunc send_response,
- unsigned int rpc_uid) {
- Send(rpc_uid, Handle(m_inst, local_id, Handle::kEntry).handle(),
- Handle(m_inst, call_uid, Handle::kRpcCall).handle(), name, params, conn,
- send_response);
-}
-
-bool RpcServer::PostRpcResponse(unsigned int local_id, unsigned int call_uid,
- std::string_view result) {
- auto thr = GetThread();
- auto i = thr->m_response_map.find(impl::RpcIdPair{local_id, call_uid});
- if (i == thr->m_response_map.end()) {
- WARNING("{}",
- "posting RPC response to nonexistent call (or duplicate response)");
- return false;
- }
- (i->getSecond())(result);
- thr->m_response_map.erase(i);
- return true;
-}
diff --git a/ntcore/src/main/native/cpp/RpcServer.h b/ntcore/src/main/native/cpp/RpcServer.h
deleted file mode 100644
index b8ae6b7..0000000
--- a/ntcore/src/main/native/cpp/RpcServer.h
+++ /dev/null
@@ -1,114 +0,0 @@
-// 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.
-
-#ifndef NTCORE_RPCSERVER_H_
-#define NTCORE_RPCSERVER_H_
-
-#include <utility>
-
-#include <wpi/CallbackManager.h>
-#include <wpi/DenseMap.h>
-#include <wpi/mutex.h>
-
-#include "Handle.h"
-#include "IRpcServer.h"
-#include "Log.h"
-
-namespace nt {
-
-namespace impl {
-
-using RpcIdPair = std::pair<unsigned int, unsigned int>;
-
-struct RpcNotifierData : public RpcAnswer {
- RpcNotifierData(NT_Entry entry_, NT_RpcCall call_, std::string_view name_,
- std::string_view params_, const ConnectionInfo& conn_,
- IRpcServer::SendResponseFunc send_response_)
- : RpcAnswer{entry_, call_, name_, params_, conn_},
- send_response{std::move(send_response_)} {}
-
- IRpcServer::SendResponseFunc send_response;
-};
-
-using RpcListenerData =
- wpi::CallbackListenerData<std::function<void(const RpcAnswer& answer)>>;
-
-class RpcServerThread
- : public wpi::CallbackThread<RpcServerThread, RpcAnswer, RpcListenerData,
- RpcNotifierData> {
- public:
- RpcServerThread(std::function<void()> on_start, std::function<void()> on_exit,
- int inst, wpi::Logger& logger)
- : CallbackThread(std::move(on_start), std::move(on_exit)),
- m_inst(inst),
- m_logger(logger) {}
-
- bool Matches(const RpcListenerData& /*listener*/,
- const RpcNotifierData& data) {
- return !data.name.empty() && data.send_response;
- }
-
- void SetListener(RpcNotifierData* data, unsigned int /*listener_uid*/) {
- unsigned int local_id = Handle{data->entry}.GetIndex();
- unsigned int call_uid = Handle{data->call}.GetIndex();
- RpcIdPair lookup_uid{local_id, call_uid};
- m_response_map.insert(std::make_pair(lookup_uid, data->send_response));
- }
-
- void DoCallback(std::function<void(const RpcAnswer& call)> callback,
- const RpcNotifierData& data) {
- DEBUG4("rpc calling {}", data.name);
- unsigned int local_id = Handle{data.entry}.GetIndex();
- unsigned int call_uid = Handle{data.call}.GetIndex();
- RpcIdPair lookup_uid{local_id, call_uid};
- callback(data);
- {
- std::scoped_lock lock(m_mutex);
- auto i = m_response_map.find(lookup_uid);
- if (i != m_response_map.end()) {
- // post an empty response and erase it
- (i->getSecond())("");
- m_response_map.erase(i);
- }
- }
- }
-
- int m_inst;
- wpi::Logger& m_logger;
- wpi::DenseMap<RpcIdPair, IRpcServer::SendResponseFunc> m_response_map;
-};
-
-} // namespace impl
-
-class RpcServer
- : public IRpcServer,
- public wpi::CallbackManager<RpcServer, impl::RpcServerThread> {
- friend class RpcServerTest;
- friend class wpi::CallbackManager<RpcServer, impl::RpcServerThread>;
-
- public:
- RpcServer(int inst, wpi::Logger& logger);
-
- void Start();
-
- unsigned int Add(std::function<void(const RpcAnswer& answer)> callback);
- unsigned int AddPolled(unsigned int poller_uid);
- void RemoveRpc(unsigned int rpc_uid) override;
-
- void ProcessRpc(unsigned int local_id, unsigned int call_uid,
- std::string_view name, std::string_view params,
- const ConnectionInfo& conn, SendResponseFunc send_response,
- unsigned int rpc_uid) override;
-
- bool PostRpcResponse(unsigned int local_id, unsigned int call_uid,
- std::string_view result);
-
- private:
- int m_inst;
- wpi::Logger& m_logger;
-};
-
-} // namespace nt
-
-#endif // NTCORE_RPCSERVER_H_
diff --git a/ntcore/src/main/native/cpp/SequenceNumber.cpp b/ntcore/src/main/native/cpp/SequenceNumber.cpp
deleted file mode 100644
index e25e636..0000000
--- a/ntcore/src/main/native/cpp/SequenceNumber.cpp
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "SequenceNumber.h"
-
-namespace nt {
-
-bool operator<(const SequenceNumber& lhs, const SequenceNumber& rhs) {
- if (lhs.m_value < rhs.m_value) {
- return (rhs.m_value - lhs.m_value) < (1u << 15);
- } else if (lhs.m_value > rhs.m_value) {
- return (lhs.m_value - rhs.m_value) > (1u << 15);
- } else {
- return false;
- }
-}
-
-bool operator>(const SequenceNumber& lhs, const SequenceNumber& rhs) {
- if (lhs.m_value < rhs.m_value) {
- return (rhs.m_value - lhs.m_value) > (1u << 15);
- } else if (lhs.m_value > rhs.m_value) {
- return (lhs.m_value - rhs.m_value) < (1u << 15);
- } else {
- return false;
- }
-}
-
-} // namespace nt
diff --git a/ntcore/src/main/native/cpp/SequenceNumber.h b/ntcore/src/main/native/cpp/SequenceNumber.h
deleted file mode 100644
index 719d85c..0000000
--- a/ntcore/src/main/native/cpp/SequenceNumber.h
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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.
-
-#ifndef NTCORE_SEQUENCENUMBER_H_
-#define NTCORE_SEQUENCENUMBER_H_
-
-namespace nt {
-
-/* A sequence number per RFC 1982 */
-class SequenceNumber {
- public:
- SequenceNumber() = default;
- explicit SequenceNumber(unsigned int value) : m_value(value) {}
- unsigned int value() const { return m_value; }
-
- SequenceNumber& operator++() {
- ++m_value;
- if (m_value > 0xffff) {
- m_value = 0;
- }
- return *this;
- }
- SequenceNumber operator++(int) {
- SequenceNumber tmp(*this);
- operator++();
- return tmp;
- }
-
- friend bool operator<(const SequenceNumber& lhs, const SequenceNumber& rhs);
- friend bool operator>(const SequenceNumber& lhs, const SequenceNumber& rhs);
- friend bool operator<=(const SequenceNumber& lhs, const SequenceNumber& rhs);
- friend bool operator>=(const SequenceNumber& lhs, const SequenceNumber& rhs);
- friend bool operator==(const SequenceNumber& lhs, const SequenceNumber& rhs);
- friend bool operator!=(const SequenceNumber& lhs, const SequenceNumber& rhs);
-
- private:
- unsigned int m_value{0};
-};
-
-bool operator<(const SequenceNumber& lhs, const SequenceNumber& rhs);
-bool operator>(const SequenceNumber& lhs, const SequenceNumber& rhs);
-
-inline bool operator<=(const SequenceNumber& lhs, const SequenceNumber& rhs) {
- return lhs == rhs || lhs < rhs;
-}
-
-inline bool operator>=(const SequenceNumber& lhs, const SequenceNumber& rhs) {
- return lhs == rhs || lhs > rhs;
-}
-
-inline bool operator==(const SequenceNumber& lhs, const SequenceNumber& rhs) {
- return lhs.m_value == rhs.m_value;
-}
-
-inline bool operator!=(const SequenceNumber& lhs, const SequenceNumber& rhs) {
- return lhs.m_value != rhs.m_value;
-}
-
-} // namespace nt
-
-#endif // NTCORE_SEQUENCENUMBER_H_
diff --git a/ntcore/src/main/native/cpp/Storage.cpp b/ntcore/src/main/native/cpp/Storage.cpp
deleted file mode 100644
index dadc3f6..0000000
--- a/ntcore/src/main/native/cpp/Storage.cpp
+++ /dev/null
@@ -1,1286 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "Storage.h"
-
-#include <wpi/StringExtras.h>
-#include <wpi/timestamp.h>
-
-#include "Handle.h"
-#include "IDispatcher.h"
-#include "IEntryNotifier.h"
-#include "INetworkConnection.h"
-#include "IRpcServer.h"
-#include "Log.h"
-
-using namespace nt;
-
-Storage::Storage(IEntryNotifier& notifier, IRpcServer& rpc_server,
- wpi::Logger& logger)
- : m_notifier(notifier), m_rpc_server(rpc_server), m_logger(logger) {
- m_terminating = false;
-}
-
-Storage::~Storage() {
- m_terminating = true;
- m_rpc_results_cond.notify_all();
-}
-
-void Storage::SetDispatcher(IDispatcher* dispatcher, bool server) {
- std::scoped_lock lock(m_mutex);
- m_dispatcher = dispatcher;
- m_server = server;
-}
-
-void Storage::ClearDispatcher() {
- m_dispatcher = nullptr;
-}
-
-NT_Type Storage::GetMessageEntryType(unsigned int id) const {
- std::scoped_lock lock(m_mutex);
- if (id >= m_idmap.size()) {
- return NT_UNASSIGNED;
- }
- Entry* entry = m_idmap[id];
- if (!entry || !entry->value) {
- return NT_UNASSIGNED;
- }
- return entry->value->type();
-}
-
-void Storage::ProcessIncoming(std::shared_ptr<Message> msg,
- INetworkConnection* conn,
- std::weak_ptr<INetworkConnection> conn_weak) {
- switch (msg->type()) {
- case Message::kKeepAlive:
- break; // ignore
- case Message::kClientHello:
- case Message::kProtoUnsup:
- case Message::kServerHelloDone:
- case Message::kServerHello:
- case Message::kClientHelloDone:
- // shouldn't get these, but ignore if we do
- break;
- case Message::kEntryAssign:
- ProcessIncomingEntryAssign(std::move(msg), conn);
- break;
- case Message::kEntryUpdate:
- ProcessIncomingEntryUpdate(std::move(msg), conn);
- break;
- case Message::kFlagsUpdate:
- ProcessIncomingFlagsUpdate(std::move(msg), conn);
- break;
- case Message::kEntryDelete:
- ProcessIncomingEntryDelete(std::move(msg), conn);
- break;
- case Message::kClearEntries:
- ProcessIncomingClearEntries(std::move(msg), conn);
- break;
- case Message::kExecuteRpc:
- ProcessIncomingExecuteRpc(std::move(msg), conn, std::move(conn_weak));
- break;
- case Message::kRpcResponse:
- ProcessIncomingRpcResponse(std::move(msg), conn);
- break;
- default:
- break;
- }
-}
-
-void Storage::ProcessIncomingEntryAssign(std::shared_ptr<Message> msg,
- INetworkConnection* conn) {
- std::unique_lock lock(m_mutex);
- unsigned int id = msg->id();
- std::string_view name = msg->str();
- Entry* entry;
- bool may_need_update = false;
- SequenceNumber seq_num(msg->seq_num_uid());
- if (m_server) {
- // if we're a server, id=0xffff requests are requests for an id
- // to be assigned, and we need to send the new assignment back to
- // the sender as well as all other connections.
- if (id == 0xffff) {
- entry = GetOrNew(name);
- // see if it was already assigned; ignore if so.
- if (entry->id != 0xffff) {
- return;
- }
-
- entry->flags = msg->flags();
- entry->seq_num = seq_num;
- SetEntryValueImpl(entry, msg->value(), lock, false);
- return;
- }
- if (id >= m_idmap.size() || !m_idmap[id]) {
- // ignore arbitrary entry assignments
- // this can happen due to e.g. assignment to deleted entry
- lock.unlock();
- DEBUG0("{}", "server: received assignment to unknown entry");
- return;
- }
- entry = m_idmap[id];
- } else {
- // clients simply accept new assignments
- if (id == 0xffff) {
- lock.unlock();
- DEBUG0("{}", "client: received entry assignment request?");
- return;
- }
- if (id >= m_idmap.size()) {
- m_idmap.resize(id + 1);
- }
- entry = m_idmap[id];
- if (!entry) {
- // create local
- entry = GetOrNew(name);
- entry->id = id;
- m_idmap[id] = entry;
- if (!entry->value) {
- // didn't exist at all (rather than just being a response to a
- // id assignment request)
- entry->value = msg->value();
- entry->flags = msg->flags();
- entry->seq_num = seq_num;
-
- // notify
- m_notifier.NotifyEntry(entry->local_id, name, entry->value,
- NT_NOTIFY_NEW);
- return;
- }
- may_need_update = true; // we may need to send an update message
-
- // if the received flags don't match what we sent, we most likely
- // updated flags locally in the interim; send flags update message.
- if (msg->flags() != entry->flags) {
- auto dispatcher = m_dispatcher;
- auto outmsg = Message::FlagsUpdate(id, entry->flags);
- lock.unlock();
- dispatcher->QueueOutgoing(outmsg, nullptr, nullptr);
- lock.lock();
- }
- }
- }
-
- // common client and server handling
-
- // already exists; ignore if sequence number not higher than local
- if (seq_num < entry->seq_num) {
- if (may_need_update) {
- auto dispatcher = m_dispatcher;
- auto outmsg =
- Message::EntryUpdate(entry->id, entry->seq_num.value(), entry->value);
- lock.unlock();
- dispatcher->QueueOutgoing(outmsg, nullptr, nullptr);
- }
- return;
- }
-
- // sanity check: name should match id
- if (msg->str() != entry->name) {
- lock.unlock();
- DEBUG0("{}", "entry assignment for same id with different name?");
- return;
- }
-
- unsigned int notify_flags = NT_NOTIFY_UPDATE;
-
- // don't update flags from a <3.0 remote (not part of message)
- // don't update flags if this is a server response to a client id request
- if (!may_need_update && conn->proto_rev() >= 0x0300) {
- // update persistent dirty flag if persistent flag changed
- if ((entry->flags & NT_PERSISTENT) != (msg->flags() & NT_PERSISTENT)) {
- m_persistent_dirty = true;
- }
- if (entry->flags != msg->flags()) {
- notify_flags |= NT_NOTIFY_FLAGS;
- }
- entry->flags = msg->flags();
- }
-
- // update persistent dirty flag if the value changed and it's persistent
- if (entry->IsPersistent() && *entry->value != *msg->value()) {
- m_persistent_dirty = true;
- }
-
- // update local
- entry->value = msg->value();
- entry->seq_num = seq_num;
-
- // notify
- m_notifier.NotifyEntry(entry->local_id, name, entry->value, notify_flags);
-
- // broadcast to all other connections (note for client there won't
- // be any other connections, so don't bother)
- if (m_server && m_dispatcher) {
- auto dispatcher = m_dispatcher;
- auto outmsg = Message::EntryAssign(entry->name, id, msg->seq_num_uid(),
- msg->value(), entry->flags);
- lock.unlock();
- dispatcher->QueueOutgoing(outmsg, nullptr, conn);
- }
-}
-
-void Storage::ProcessIncomingEntryUpdate(std::shared_ptr<Message> msg,
- INetworkConnection* conn) {
- std::unique_lock lock(m_mutex);
- unsigned int id = msg->id();
- if (id >= m_idmap.size() || !m_idmap[id]) {
- // ignore arbitrary entry updates;
- // this can happen due to deleted entries
- lock.unlock();
- DEBUG0("{}", "received update to unknown entry");
- return;
- }
- Entry* entry = m_idmap[id];
-
- // ignore if sequence number not higher than local
- SequenceNumber seq_num(msg->seq_num_uid());
- if (seq_num <= entry->seq_num) {
- return;
- }
-
- // update local
- entry->value = msg->value();
- entry->seq_num = seq_num;
-
- // update persistent dirty flag if it's a persistent value
- if (entry->IsPersistent()) {
- m_persistent_dirty = true;
- }
-
- // notify
- m_notifier.NotifyEntry(entry->local_id, entry->name, entry->value,
- NT_NOTIFY_UPDATE);
-
- // broadcast to all other connections (note for client there won't
- // be any other connections, so don't bother)
- if (m_server && m_dispatcher) {
- auto dispatcher = m_dispatcher;
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, conn);
- }
-}
-
-void Storage::ProcessIncomingFlagsUpdate(std::shared_ptr<Message> msg,
- INetworkConnection* conn) {
- std::unique_lock lock(m_mutex);
- unsigned int id = msg->id();
- if (id >= m_idmap.size() || !m_idmap[id]) {
- // ignore arbitrary entry updates;
- // this can happen due to deleted entries
- lock.unlock();
- DEBUG0("{}", "received flags update to unknown entry");
- return;
- }
-
- // update local
- SetEntryFlagsImpl(m_idmap[id], msg->flags(), lock, false);
-
- // broadcast to all other connections (note for client there won't
- // be any other connections, so don't bother)
- if (m_server && m_dispatcher) {
- auto dispatcher = m_dispatcher;
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, conn);
- }
-}
-
-void Storage::ProcessIncomingEntryDelete(std::shared_ptr<Message> msg,
- INetworkConnection* conn) {
- std::unique_lock lock(m_mutex);
- unsigned int id = msg->id();
- if (id >= m_idmap.size() || !m_idmap[id]) {
- // ignore arbitrary entry updates;
- // this can happen due to deleted entries
- lock.unlock();
- DEBUG0("{}", "received delete to unknown entry");
- return;
- }
-
- // update local
- DeleteEntryImpl(m_idmap[id], lock, false);
-
- // broadcast to all other connections (note for client there won't
- // be any other connections, so don't bother)
- if (m_server && m_dispatcher) {
- auto dispatcher = m_dispatcher;
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, conn);
- }
-}
-
-void Storage::ProcessIncomingClearEntries(std::shared_ptr<Message> msg,
- INetworkConnection* conn) {
- std::unique_lock lock(m_mutex);
- // update local
- DeleteAllEntriesImpl(false);
-
- // broadcast to all other connections (note for client there won't
- // be any other connections, so don't bother)
- if (m_server && m_dispatcher) {
- auto dispatcher = m_dispatcher;
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, conn);
- }
-}
-
-void Storage::ProcessIncomingExecuteRpc(
- std::shared_ptr<Message> msg, INetworkConnection* /*conn*/,
- std::weak_ptr<INetworkConnection> conn_weak) {
- std::unique_lock lock(m_mutex);
- if (!m_server) {
- return; // only process on server
- }
- unsigned int id = msg->id();
- if (id >= m_idmap.size() || !m_idmap[id]) {
- // ignore call to non-existent RPC
- // this can happen due to deleted entries
- lock.unlock();
- DEBUG0("{}", "received RPC call to unknown entry");
- return;
- }
- Entry* entry = m_idmap[id];
- if (!entry->value || !entry->value->IsRpc()) {
- lock.unlock();
- DEBUG0("{}", "received RPC call to non-RPC entry");
- return;
- }
- ConnectionInfo conn_info;
- auto c = conn_weak.lock();
- if (c) {
- conn_info = c->info();
- } else {
- conn_info.remote_id = "";
- conn_info.remote_ip = "";
- conn_info.remote_port = 0;
- conn_info.last_update = 0;
- conn_info.protocol_version = 0;
- }
- unsigned int call_uid = msg->seq_num_uid();
- m_rpc_server.ProcessRpc(
- entry->local_id, call_uid, entry->name, msg->str(), conn_info,
- [=](std::string_view result) {
- auto c = conn_weak.lock();
- if (c) {
- c->QueueOutgoing(Message::RpcResponse(id, call_uid, result));
- }
- },
- entry->rpc_uid);
-}
-
-void Storage::ProcessIncomingRpcResponse(std::shared_ptr<Message> msg,
- INetworkConnection* /*conn*/) {
- std::unique_lock lock(m_mutex);
- if (m_server) {
- return; // only process on client
- }
- unsigned int id = msg->id();
- if (id >= m_idmap.size() || !m_idmap[id]) {
- // ignore response to non-existent RPC
- // this can happen due to deleted entries
- lock.unlock();
- DEBUG0("{}", "received rpc response to unknown entry");
- return;
- }
- Entry* entry = m_idmap[id];
- if (!entry->value || !entry->value->IsRpc()) {
- lock.unlock();
- DEBUG0("{}", "received RPC response to non-RPC entry");
- return;
- }
- m_rpc_results.insert({RpcIdPair{entry->local_id, msg->seq_num_uid()},
- std::string{msg->str()}});
- m_rpc_results_cond.notify_all();
-}
-
-void Storage::GetInitialAssignments(
- INetworkConnection& conn, std::vector<std::shared_ptr<Message>>* msgs) {
- std::scoped_lock lock(m_mutex);
- conn.set_state(INetworkConnection::kSynchronized);
- for (auto& i : m_entries) {
- Entry* entry = i.getValue();
- if (!entry->value) {
- continue;
- }
- msgs->emplace_back(Message::EntryAssign(i.getKey(), entry->id,
- entry->seq_num.value(),
- entry->value, entry->flags));
- }
-}
-
-void Storage::ApplyInitialAssignments(
- INetworkConnection& conn, wpi::span<std::shared_ptr<Message>> msgs,
- bool /*new_server*/, std::vector<std::shared_ptr<Message>>* out_msgs) {
- std::unique_lock lock(m_mutex);
- if (m_server) {
- return; // should not do this on server
- }
-
- conn.set_state(INetworkConnection::kSynchronized);
-
- std::vector<std::shared_ptr<Message>> update_msgs;
-
- // clear existing id's
- for (auto& i : m_entries) {
- i.getValue()->id = 0xffff;
- }
-
- // clear existing idmap
- m_idmap.resize(0);
-
- // apply assignments
- for (auto& msg : msgs) {
- if (!msg->Is(Message::kEntryAssign)) {
- DEBUG0("{}", "client: received non-entry assignment request?");
- continue;
- }
-
- unsigned int id = msg->id();
- if (id == 0xffff) {
- DEBUG0("{}", "client: received entry assignment request?");
- continue;
- }
-
- SequenceNumber seq_num(msg->seq_num_uid());
- std::string_view name = msg->str();
-
- Entry* entry = GetOrNew(name);
- entry->seq_num = seq_num;
- entry->id = id;
- if (!entry->value) {
- // doesn't currently exist
- entry->value = msg->value();
- entry->flags = msg->flags();
- // notify
- m_notifier.NotifyEntry(entry->local_id, name, entry->value,
- NT_NOTIFY_NEW);
- } else {
- // if we have written the value locally and the value is not persistent,
- // then we don't update the local value and instead send it back to the
- // server as an update message
- if (entry->local_write && !entry->IsPersistent()) {
- ++entry->seq_num;
- update_msgs.emplace_back(Message::EntryUpdate(
- entry->id, entry->seq_num.value(), entry->value));
- } else {
- entry->value = msg->value();
- unsigned int notify_flags = NT_NOTIFY_UPDATE;
- // don't update flags from a <3.0 remote (not part of message)
- if (conn.proto_rev() >= 0x0300) {
- if (entry->flags != msg->flags()) {
- notify_flags |= NT_NOTIFY_FLAGS;
- }
- entry->flags = msg->flags();
- }
- // notify
- m_notifier.NotifyEntry(entry->local_id, name, entry->value,
- notify_flags);
- }
- }
-
- // save to idmap
- if (id >= m_idmap.size()) {
- m_idmap.resize(id + 1);
- }
- m_idmap[id] = entry;
- }
-
- // delete or generate assign messages for unassigned local entries
- DeleteAllEntriesImpl(false, [&](Entry* entry) -> bool {
- // was assigned by the server, don't delete
- if (entry->id != 0xffff) {
- return false;
- }
- // if we have written the value locally, we send an assign message to the
- // server instead of deleting
- if (entry->local_write) {
- out_msgs->emplace_back(Message::EntryAssign(entry->name, entry->id,
- entry->seq_num.value(),
- entry->value, entry->flags));
- return false;
- }
- // otherwise delete
- return true;
- });
- auto dispatcher = m_dispatcher;
- lock.unlock();
- for (auto& msg : update_msgs) {
- dispatcher->QueueOutgoing(msg, nullptr, nullptr);
- }
-}
-
-std::shared_ptr<Value> Storage::GetEntryValue(std::string_view name) const {
- std::scoped_lock lock(m_mutex);
- auto i = m_entries.find(name);
- if (i == m_entries.end()) {
- return nullptr;
- }
- return i->getValue()->value;
-}
-
-std::shared_ptr<Value> Storage::GetEntryValue(unsigned int local_id) const {
- std::scoped_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return nullptr;
- }
- return m_localmap[local_id]->value;
-}
-
-bool Storage::SetDefaultEntryValue(std::string_view name,
- std::shared_ptr<Value> value) {
- if (name.empty()) {
- return false;
- }
- if (!value) {
- return false;
- }
- std::unique_lock lock(m_mutex);
- Entry* entry = GetOrNew(name);
-
- // we return early if value already exists; if types match return true
- if (entry->value) {
- return entry->value->type() == value->type();
- }
-
- SetEntryValueImpl(entry, value, lock, true);
- return true;
-}
-
-bool Storage::SetDefaultEntryValue(unsigned int local_id,
- std::shared_ptr<Value> value) {
- if (!value) {
- return false;
- }
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return false;
- }
- Entry* entry = m_localmap[local_id].get();
-
- // we return early if value already exists; if types match return true
- if (entry->value) {
- return entry->value->type() == value->type();
- }
-
- SetEntryValueImpl(entry, value, lock, true);
- return true;
-}
-
-bool Storage::SetEntryValue(std::string_view name,
- std::shared_ptr<Value> value) {
- if (name.empty()) {
- return true;
- }
- if (!value) {
- return true;
- }
- std::unique_lock lock(m_mutex);
- Entry* entry = GetOrNew(name);
-
- if (entry->value && entry->value->type() != value->type()) {
- return false; // error on type mismatch
- }
-
- SetEntryValueImpl(entry, value, lock, true);
- return true;
-}
-
-bool Storage::SetEntryValue(unsigned int local_id,
- std::shared_ptr<Value> value) {
- if (!value) {
- return true;
- }
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return true;
- }
- Entry* entry = m_localmap[local_id].get();
-
- if (entry->value && entry->value->type() != value->type()) {
- return false; // error on type mismatch
- }
-
- SetEntryValueImpl(entry, value, lock, true);
- return true;
-}
-
-void Storage::SetEntryValueImpl(Entry* entry, std::shared_ptr<Value> value,
- std::unique_lock<wpi::mutex>& lock,
- bool local) {
- if (!value) {
- return;
- }
- auto old_value = entry->value;
- entry->value = value;
-
- // if we're the server, assign an id if it doesn't have one
- if (m_server && entry->id == 0xffff) {
- unsigned int id = m_idmap.size();
- entry->id = id;
- m_idmap.push_back(entry);
- }
-
- // update persistent dirty flag if value changed and it's persistent
- if (entry->IsPersistent() && (!old_value || *old_value != *value)) {
- m_persistent_dirty = true;
- }
-
- // notify
- if (!old_value) {
- m_notifier.NotifyEntry(entry->local_id, entry->name, value,
- NT_NOTIFY_NEW | (local ? NT_NOTIFY_LOCAL : 0));
- } else if (*old_value != *value) {
- m_notifier.NotifyEntry(entry->local_id, entry->name, value,
- NT_NOTIFY_UPDATE | (local ? NT_NOTIFY_LOCAL : 0));
- }
-
- // remember local changes
- if (local) {
- entry->local_write = true;
- }
-
- // generate message
- if (!m_dispatcher || (!local && !m_server)) {
- return;
- }
- auto dispatcher = m_dispatcher;
- if (!old_value || old_value->type() != value->type()) {
- if (local) {
- ++entry->seq_num;
- }
- auto msg = Message::EntryAssign(
- entry->name, entry->id, entry->seq_num.value(), value, entry->flags);
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, nullptr);
- } else if (*old_value != *value) {
- if (local) {
- ++entry->seq_num;
- }
- // don't send an update if we don't have an assigned id yet
- if (entry->id != 0xffff) {
- auto msg = Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, nullptr);
- }
- }
-}
-
-void Storage::SetEntryTypeValue(std::string_view name,
- std::shared_ptr<Value> value) {
- if (name.empty()) {
- return;
- }
- if (!value) {
- return;
- }
- std::unique_lock lock(m_mutex);
- Entry* entry = GetOrNew(name);
-
- SetEntryValueImpl(entry, value, lock, true);
-}
-
-void Storage::SetEntryTypeValue(unsigned int local_id,
- std::shared_ptr<Value> value) {
- if (!value) {
- return;
- }
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return;
- }
- Entry* entry = m_localmap[local_id].get();
- if (!entry) {
- return;
- }
-
- SetEntryValueImpl(entry, value, lock, true);
-}
-
-void Storage::SetEntryFlags(std::string_view name, unsigned int flags) {
- if (name.empty()) {
- return;
- }
- std::unique_lock lock(m_mutex);
- auto i = m_entries.find(name);
- if (i == m_entries.end()) {
- return;
- }
- SetEntryFlagsImpl(i->getValue(), flags, lock, true);
-}
-
-void Storage::SetEntryFlags(unsigned int id_local, unsigned int flags) {
- std::unique_lock lock(m_mutex);
- if (id_local >= m_localmap.size()) {
- return;
- }
- SetEntryFlagsImpl(m_localmap[id_local].get(), flags, lock, true);
-}
-
-void Storage::SetEntryFlagsImpl(Entry* entry, unsigned int flags,
- std::unique_lock<wpi::mutex>& lock,
- bool local) {
- if (!entry->value || entry->flags == flags) {
- return;
- }
-
- // update persistent dirty flag if persistent flag changed
- if ((entry->flags & NT_PERSISTENT) != (flags & NT_PERSISTENT)) {
- m_persistent_dirty = true;
- }
-
- entry->flags = flags;
-
- // notify
- m_notifier.NotifyEntry(entry->local_id, entry->name, entry->value,
- NT_NOTIFY_FLAGS | (local ? NT_NOTIFY_LOCAL : 0));
-
- // generate message
- if (!local || !m_dispatcher) {
- return;
- }
- auto dispatcher = m_dispatcher;
- unsigned int id = entry->id;
- // don't send an update if we don't have an assigned id yet
- if (id != 0xffff) {
- lock.unlock();
- dispatcher->QueueOutgoing(Message::FlagsUpdate(id, flags), nullptr,
- nullptr);
- }
-}
-
-unsigned int Storage::GetEntryFlags(std::string_view name) const {
- std::scoped_lock lock(m_mutex);
- auto i = m_entries.find(name);
- if (i == m_entries.end()) {
- return 0;
- }
- return i->getValue()->flags;
-}
-
-unsigned int Storage::GetEntryFlags(unsigned int local_id) const {
- std::scoped_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return 0;
- }
- return m_localmap[local_id]->flags;
-}
-
-void Storage::DeleteEntry(std::string_view name) {
- std::unique_lock lock(m_mutex);
- auto i = m_entries.find(name);
- if (i == m_entries.end()) {
- return;
- }
- DeleteEntryImpl(i->getValue(), lock, true);
-}
-
-void Storage::DeleteEntry(unsigned int local_id) {
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return;
- }
- DeleteEntryImpl(m_localmap[local_id].get(), lock, true);
-}
-
-void Storage::DeleteEntryImpl(Entry* entry, std::unique_lock<wpi::mutex>& lock,
- bool local) {
- unsigned int id = entry->id;
-
- // Erase entry from id mapping.
- if (id < m_idmap.size()) {
- m_idmap[id] = nullptr;
- }
-
- // empty the value and reset id and local_write flag
- std::shared_ptr<Value> old_value;
- old_value.swap(entry->value);
- entry->id = 0xffff;
- entry->local_write = false;
-
- // remove RPC if there was one
- if (entry->rpc_uid != UINT_MAX) {
- m_rpc_server.RemoveRpc(entry->rpc_uid);
- entry->rpc_uid = UINT_MAX;
- }
-
- // update persistent dirty flag if it's a persistent value
- if (entry->IsPersistent()) {
- m_persistent_dirty = true;
- }
-
- // reset flags
- entry->flags = 0;
-
- if (!old_value) {
- return; // was not previously assigned
- }
-
- // notify
- m_notifier.NotifyEntry(entry->local_id, entry->name, old_value,
- NT_NOTIFY_DELETE | (local ? NT_NOTIFY_LOCAL : 0));
-
- // if it had a value, generate message
- // don't send an update if we don't have an assigned id yet
- if (local && id != 0xffff) {
- if (!m_dispatcher) {
- return;
- }
- auto dispatcher = m_dispatcher;
- lock.unlock();
- dispatcher->QueueOutgoing(Message::EntryDelete(id), nullptr, nullptr);
- }
-}
-
-template <typename F>
-void Storage::DeleteAllEntriesImpl(bool local, F should_delete) {
- for (auto& i : m_entries) {
- Entry* entry = i.getValue();
- if (entry->value && should_delete(entry)) {
- // notify it's being deleted
- m_notifier.NotifyEntry(entry->local_id, i.getKey(), entry->value,
- NT_NOTIFY_DELETE | (local ? NT_NOTIFY_LOCAL : 0));
- // remove it from idmap
- if (entry->id < m_idmap.size()) {
- m_idmap[entry->id] = nullptr;
- }
- entry->id = 0xffff;
- entry->local_write = false;
- entry->value.reset();
- continue;
- }
- }
-}
-
-void Storage::DeleteAllEntriesImpl(bool local) {
- // only delete non-persistent values
- DeleteAllEntriesImpl(local,
- [](Entry* entry) { return !entry->IsPersistent(); });
-}
-
-void Storage::DeleteAllEntries() {
- std::unique_lock lock(m_mutex);
- if (m_entries.empty()) {
- return;
- }
-
- DeleteAllEntriesImpl(true);
-
- // generate message
- if (!m_dispatcher) {
- return;
- }
- auto dispatcher = m_dispatcher;
- lock.unlock();
- dispatcher->QueueOutgoing(Message::ClearEntries(), nullptr, nullptr);
-}
-
-Storage::Entry* Storage::GetOrNew(std::string_view name) {
- auto& entry = m_entries[name];
- if (!entry) {
- m_localmap.emplace_back(new Entry(name));
- entry = m_localmap.back().get();
- entry->local_id = m_localmap.size() - 1;
- }
- return entry;
-}
-
-unsigned int Storage::GetEntry(std::string_view name) {
- if (name.empty()) {
- return UINT_MAX;
- }
- std::unique_lock lock(m_mutex);
- return GetOrNew(name)->local_id;
-}
-
-std::vector<unsigned int> Storage::GetEntries(std::string_view prefix,
- unsigned int types) {
- std::scoped_lock lock(m_mutex);
- std::vector<unsigned int> ids;
- for (auto& i : m_entries) {
- Entry* entry = i.getValue();
- auto value = entry->value.get();
- if (!value || !wpi::starts_with(i.getKey(), prefix)) {
- continue;
- }
- if (types != 0 && (types & value->type()) == 0) {
- continue;
- }
- ids.push_back(entry->local_id);
- }
- return ids;
-}
-
-EntryInfo Storage::GetEntryInfo(int inst, unsigned int local_id) const {
- EntryInfo info;
- info.entry = 0;
- info.type = NT_UNASSIGNED;
- info.flags = 0;
- info.last_change = 0;
-
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return info;
- }
- Entry* entry = m_localmap[local_id].get();
- if (!entry->value) {
- return info;
- }
-
- info.entry = Handle(inst, local_id, Handle::kEntry);
- info.name = entry->name;
- info.type = entry->value->type();
- info.flags = entry->flags;
- info.last_change = entry->value->last_change();
- return info;
-}
-
-std::string Storage::GetEntryName(unsigned int local_id) const {
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return {};
- }
- return m_localmap[local_id]->name;
-}
-
-NT_Type Storage::GetEntryType(unsigned int local_id) const {
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return NT_UNASSIGNED;
- }
- Entry* entry = m_localmap[local_id].get();
- if (!entry->value) {
- return NT_UNASSIGNED;
- }
- return entry->value->type();
-}
-
-uint64_t Storage::GetEntryLastChange(unsigned int local_id) const {
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return 0;
- }
- Entry* entry = m_localmap[local_id].get();
- if (!entry->value) {
- return 0;
- }
- return entry->value->last_change();
-}
-
-std::vector<EntryInfo> Storage::GetEntryInfo(int inst, std::string_view prefix,
- unsigned int types) {
- std::scoped_lock lock(m_mutex);
- std::vector<EntryInfo> infos;
- for (auto& i : m_entries) {
- Entry* entry = i.getValue();
- auto value = entry->value.get();
- if (!value || !wpi::starts_with(i.getKey(), prefix)) {
- continue;
- }
- if (types != 0 && (types & value->type()) == 0) {
- continue;
- }
- EntryInfo info;
- info.entry = Handle(inst, entry->local_id, Handle::kEntry);
- info.name = i.getKey();
- info.type = value->type();
- info.flags = entry->flags;
- info.last_change = value->last_change();
- infos.push_back(std::move(info));
- }
- return infos;
-}
-
-unsigned int Storage::AddListener(
- std::string_view prefix,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const {
- std::scoped_lock lock(m_mutex);
- unsigned int uid = m_notifier.Add(callback, prefix, flags);
- // perform immediate notifications
- if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0) {
- for (auto& i : m_entries) {
- Entry* entry = i.getValue();
- if (!entry->value || !wpi::starts_with(i.getKey(), prefix)) {
- continue;
- }
- m_notifier.NotifyEntry(entry->local_id, i.getKey(), entry->value,
- NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
- }
- }
- return uid;
-}
-
-unsigned int Storage::AddListener(
- unsigned int local_id,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const {
- std::scoped_lock lock(m_mutex);
- unsigned int uid = m_notifier.Add(callback, local_id, flags);
- // perform immediate notifications
- if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0 &&
- local_id < m_localmap.size()) {
- Entry* entry = m_localmap[local_id].get();
- if (entry->value) {
- m_notifier.NotifyEntry(local_id, entry->name, entry->value,
- NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
- }
- }
- return uid;
-}
-
-unsigned int Storage::AddPolledListener(unsigned int poller,
- std::string_view prefix,
- unsigned int flags) const {
- std::scoped_lock lock(m_mutex);
- unsigned int uid = m_notifier.AddPolled(poller, prefix, flags);
- // perform immediate notifications
- if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0) {
- for (auto& i : m_entries) {
- if (!wpi::starts_with(i.getKey(), prefix)) {
- continue;
- }
- Entry* entry = i.getValue();
- if (!entry->value) {
- continue;
- }
- m_notifier.NotifyEntry(entry->local_id, i.getKey(), entry->value,
- NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
- }
- }
- return uid;
-}
-
-unsigned int Storage::AddPolledListener(unsigned int poller,
- unsigned int local_id,
- unsigned int flags) const {
- std::scoped_lock lock(m_mutex);
- unsigned int uid = m_notifier.AddPolled(poller, local_id, flags);
- // perform immediate notifications
- if ((flags & NT_NOTIFY_IMMEDIATE) != 0 && (flags & NT_NOTIFY_NEW) != 0 &&
- local_id < m_localmap.size()) {
- Entry* entry = m_localmap[local_id].get();
- // if no value, don't notify
- if (entry->value) {
- m_notifier.NotifyEntry(local_id, entry->name, entry->value,
- NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, uid);
- }
- }
- return uid;
-}
-
-bool Storage::GetPersistentEntries(
- bool periodic,
- std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
- const {
- // copy values out of storage as quickly as possible so lock isn't held
- {
- std::scoped_lock lock(m_mutex);
- // for periodic, don't re-save unless something has changed
- if (periodic && !m_persistent_dirty) {
- return false;
- }
- m_persistent_dirty = false;
- entries->reserve(m_entries.size());
- for (auto& i : m_entries) {
- Entry* entry = i.getValue();
- // only write persistent-flagged values
- if (!entry->value || !entry->IsPersistent()) {
- continue;
- }
- entries->emplace_back(i.getKey(), entry->value);
- }
- }
-
- // sort in name order
- std::sort(entries->begin(), entries->end(),
- [](const std::pair<std::string, std::shared_ptr<Value>>& a,
- const std::pair<std::string, std::shared_ptr<Value>>& b) {
- return a.first < b.first;
- });
- return true;
-}
-
-bool Storage::GetEntries(
- std::string_view prefix,
- std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
- const {
- // copy values out of storage as quickly as possible so lock isn't held
- {
- std::scoped_lock lock(m_mutex);
- entries->reserve(m_entries.size());
- for (auto& i : m_entries) {
- Entry* entry = i.getValue();
- // only write values with given prefix
- if (!entry->value || !wpi::starts_with(i.getKey(), prefix)) {
- continue;
- }
- entries->emplace_back(i.getKey(), entry->value);
- }
- }
-
- // sort in name order
- std::sort(entries->begin(), entries->end(),
- [](const std::pair<std::string, std::shared_ptr<Value>>& a,
- const std::pair<std::string, std::shared_ptr<Value>>& b) {
- return a.first < b.first;
- });
- return true;
-}
-
-void Storage::CreateRpc(unsigned int local_id, std::string_view def,
- unsigned int rpc_uid) {
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return;
- }
- Entry* entry = m_localmap[local_id].get();
-
- auto old_value = entry->value;
- auto value = Value::MakeRpc(def);
- entry->value = value;
-
- // set up the RPC info
- entry->rpc_uid = rpc_uid;
-
- if (old_value && *old_value == *value) {
- return;
- }
-
- // assign an id if it doesn't have one
- if (entry->id == 0xffff) {
- unsigned int id = m_idmap.size();
- entry->id = id;
- m_idmap.push_back(entry);
- }
-
- // generate message
- if (!m_dispatcher) {
- return;
- }
- auto dispatcher = m_dispatcher;
- if (!old_value || old_value->type() != value->type()) {
- ++entry->seq_num;
- auto msg = Message::EntryAssign(
- entry->name, entry->id, entry->seq_num.value(), value, entry->flags);
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, nullptr);
- } else {
- ++entry->seq_num;
- auto msg = Message::EntryUpdate(entry->id, entry->seq_num.value(), value);
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, nullptr);
- }
-}
-
-unsigned int Storage::CallRpc(unsigned int local_id, std::string_view params) {
- std::unique_lock lock(m_mutex);
- if (local_id >= m_localmap.size()) {
- return 0;
- }
- Entry* entry = m_localmap[local_id].get();
-
- if (!entry->value || !entry->value->IsRpc()) {
- return 0;
- }
-
- ++entry->rpc_call_uid;
- if (entry->rpc_call_uid > 0xffff) {
- entry->rpc_call_uid = 0;
- }
- unsigned int call_uid = entry->rpc_call_uid;
-
- auto msg = Message::ExecuteRpc(entry->id, call_uid, params);
- std::string_view name{entry->name};
-
- if (m_server) {
- // RPCs are unlikely to be used locally on the server, but handle it
- // gracefully anyway.
- auto rpc_uid = entry->rpc_uid;
- lock.unlock();
- ConnectionInfo conn_info;
- conn_info.remote_id = "Server";
- conn_info.remote_ip = "localhost";
- conn_info.remote_port = 0;
- conn_info.last_update = wpi::Now();
- conn_info.protocol_version = 0x0300;
- unsigned int call_uid = msg->seq_num_uid();
- m_rpc_server.ProcessRpc(
- local_id, call_uid, name, msg->str(), conn_info,
- [=](std::string_view result) {
- std::scoped_lock lock(m_mutex);
- m_rpc_results.insert(std::make_pair(RpcIdPair{local_id, call_uid},
- std::string{result}));
- m_rpc_results_cond.notify_all();
- },
- rpc_uid);
- } else {
- auto dispatcher = m_dispatcher;
- lock.unlock();
- dispatcher->QueueOutgoing(msg, nullptr, nullptr);
- }
- return call_uid;
-}
-
-bool Storage::GetRpcResult(unsigned int local_id, unsigned int call_uid,
- std::string* result) {
- bool timed_out = false;
- return GetRpcResult(local_id, call_uid, result, -1, &timed_out);
-}
-
-bool Storage::GetRpcResult(unsigned int local_id, unsigned int call_uid,
- std::string* result, double timeout,
- bool* timed_out) {
- std::unique_lock lock(m_mutex);
-
- RpcIdPair call_pair{local_id, call_uid};
-
- // only allow one blocking call per rpc call uid
- if (!m_rpc_blocking_calls.insert(call_pair).second) {
- return false;
- }
-
- auto timeout_time =
- std::chrono::steady_clock::now() + std::chrono::duration<double>(timeout);
- *timed_out = false;
- for (;;) {
- auto i = m_rpc_results.find(call_pair);
- if (i == m_rpc_results.end()) {
- if (timeout == 0 || m_terminating) {
- m_rpc_blocking_calls.erase(call_pair);
- return false;
- }
- if (timeout < 0) {
- m_rpc_results_cond.wait(lock);
- } else {
- auto cond_timed_out = m_rpc_results_cond.wait_until(lock, timeout_time);
- if (cond_timed_out == std::cv_status::timeout) {
- m_rpc_blocking_calls.erase(call_pair);
- *timed_out = true;
- return false;
- }
- }
- // if element does not exist, we have been canceled
- if (m_rpc_blocking_calls.count(call_pair) == 0) {
- return false;
- }
- if (m_terminating) {
- m_rpc_blocking_calls.erase(call_pair);
- return false;
- }
- continue;
- }
- result->swap(i->getSecond());
- // safe to erase even if id does not exist
- m_rpc_blocking_calls.erase(call_pair);
- m_rpc_results.erase(i);
- return true;
- }
-}
-
-void Storage::CancelRpcResult(unsigned int local_id, unsigned int call_uid) {
- std::unique_lock lock(m_mutex);
- // safe to erase even if id does not exist
- m_rpc_blocking_calls.erase(RpcIdPair{local_id, call_uid});
- m_rpc_results_cond.notify_all();
-}
diff --git a/ntcore/src/main/native/cpp/Storage.h b/ntcore/src/main/native/cpp/Storage.h
deleted file mode 100644
index f49c071..0000000
--- a/ntcore/src/main/native/cpp/Storage.h
+++ /dev/null
@@ -1,267 +0,0 @@
-// 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.
-
-#ifndef NTCORE_STORAGE_H_
-#define NTCORE_STORAGE_H_
-
-#include <stdint.h>
-
-#include <atomic>
-#include <cstddef>
-#include <functional>
-#include <memory>
-#include <string>
-#include <string_view>
-#include <utility>
-#include <vector>
-
-#include <wpi/DenseMap.h>
-#include <wpi/SmallSet.h>
-#include <wpi/StringMap.h>
-#include <wpi/condition_variable.h>
-#include <wpi/mutex.h>
-#include <wpi/span.h>
-
-#include "IStorage.h"
-#include "Message.h"
-#include "SequenceNumber.h"
-#include "ntcore_cpp.h"
-
-namespace wpi {
-class Logger;
-class raw_istream;
-class raw_ostream;
-} // namespace wpi
-
-namespace nt {
-
-class IEntryNotifier;
-class INetworkConnection;
-class IRpcServer;
-class IStorageTest;
-
-class Storage : public IStorage {
- friend class StorageTest;
-
- public:
- Storage(IEntryNotifier& notifier, IRpcServer& rpcserver, wpi::Logger& logger);
- Storage(const Storage&) = delete;
- Storage& operator=(const Storage&) = delete;
-
- ~Storage() override;
-
- // Accessors required by Dispatcher. An interface is used for
- // generation of outgoing messages to break a dependency loop between
- // Storage and Dispatcher.
- void SetDispatcher(IDispatcher* dispatcher, bool server) override;
- void ClearDispatcher() override;
-
- // Required for wire protocol 2.0 to get the entry type of an entry when
- // receiving entry updates (because the length/type is not provided in the
- // message itself). Not used in wire protocol 3.0.
- NT_Type GetMessageEntryType(unsigned int id) const override;
-
- void ProcessIncoming(std::shared_ptr<Message> msg, INetworkConnection* conn,
- std::weak_ptr<INetworkConnection> conn_weak) override;
- void GetInitialAssignments(
- INetworkConnection& conn,
- std::vector<std::shared_ptr<Message>>* msgs) override;
- void ApplyInitialAssignments(
- INetworkConnection& conn, wpi::span<std::shared_ptr<Message>> msgs,
- bool new_server,
- std::vector<std::shared_ptr<Message>>* out_msgs) override;
-
- // User functions. These are the actual implementations of the corresponding
- // user API functions in ntcore_cpp.
- std::shared_ptr<Value> GetEntryValue(std::string_view name) const;
- std::shared_ptr<Value> GetEntryValue(unsigned int local_id) const;
-
- bool SetDefaultEntryValue(std::string_view name,
- std::shared_ptr<Value> value);
- bool SetDefaultEntryValue(unsigned int local_id,
- std::shared_ptr<Value> value);
-
- bool SetEntryValue(std::string_view name, std::shared_ptr<Value> value);
- bool SetEntryValue(unsigned int local_id, std::shared_ptr<Value> value);
-
- void SetEntryTypeValue(std::string_view name, std::shared_ptr<Value> value);
- void SetEntryTypeValue(unsigned int local_id, std::shared_ptr<Value> value);
-
- void SetEntryFlags(std::string_view name, unsigned int flags);
- void SetEntryFlags(unsigned int local_id, unsigned int flags);
-
- unsigned int GetEntryFlags(std::string_view name) const;
- unsigned int GetEntryFlags(unsigned int local_id) const;
-
- void DeleteEntry(std::string_view name);
- void DeleteEntry(unsigned int local_id);
-
- void DeleteAllEntries();
-
- std::vector<EntryInfo> GetEntryInfo(int inst, std::string_view prefix,
- unsigned int types);
-
- unsigned int AddListener(
- std::string_view prefix,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const;
- unsigned int AddListener(
- unsigned int local_id,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const;
-
- unsigned int AddPolledListener(unsigned int poller_uid,
- std::string_view prefix,
- unsigned int flags) const;
- unsigned int AddPolledListener(unsigned int poller_uid, unsigned int local_id,
- unsigned int flags) const;
-
- // Index-only
- unsigned int GetEntry(std::string_view name);
- std::vector<unsigned int> GetEntries(std::string_view prefix,
- unsigned int types);
- EntryInfo GetEntryInfo(int inst, unsigned int local_id) const;
- std::string GetEntryName(unsigned int local_id) const;
- NT_Type GetEntryType(unsigned int local_id) const;
- uint64_t GetEntryLastChange(unsigned int local_id) const;
-
- // Filename-based save/load functions. Used both by periodic saves and
- // accessible directly via the user API.
- const char* SavePersistent(std::string_view filename,
- bool periodic) const override;
- const char* LoadPersistent(
- std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn) override;
-
- const char* SaveEntries(std::string_view filename,
- std::string_view prefix) const;
- const char* LoadEntries(
- std::string_view filename, std::string_view prefix,
- std::function<void(size_t line, const char* msg)> warn);
-
- // Stream-based save/load functions (exposed for testing purposes). These
- // implement the guts of the filename-based functions.
- void SavePersistent(wpi::raw_ostream& os, bool periodic) const;
- bool LoadEntries(wpi::raw_istream& is, std::string_view prefix,
- bool persistent,
- std::function<void(size_t line, const char* msg)> warn);
-
- void SaveEntries(wpi::raw_ostream& os, std::string_view prefix) const;
-
- // RPC configuration needs to come through here as RPC definitions are
- // actually special Storage value types.
- void CreateRpc(unsigned int local_id, std::string_view def,
- unsigned int rpc_uid);
- unsigned int CallRpc(unsigned int local_id, std::string_view params);
- bool GetRpcResult(unsigned int local_id, unsigned int call_uid,
- std::string* result);
- bool GetRpcResult(unsigned int local_id, unsigned int call_uid,
- std::string* result, double timeout, bool* timed_out);
- void CancelRpcResult(unsigned int local_id, unsigned int call_uid);
-
- private:
- // Data for each table entry.
- struct Entry {
- explicit Entry(std::string_view name_) : name(name_) {}
- bool IsPersistent() const { return (flags & NT_PERSISTENT) != 0; }
-
- // We redundantly store the name so that it's available when accessing the
- // raw Entry* via the ID map.
- std::string name;
-
- // The current value and flags.
- std::shared_ptr<Value> value;
- unsigned int flags{0};
-
- // Unique ID for this entry as used in network messages. The value is
- // assigned by the server, so on the client this is 0xffff until an
- // entry assignment is received back from the server.
- unsigned int id{0xffff};
-
- // Local ID.
- unsigned int local_id{UINT_MAX};
-
- // Sequence number for update resolution.
- SequenceNumber seq_num;
-
- // If value has been written locally. Used during initial handshake
- // on client to determine whether or not to accept remote changes.
- bool local_write{false};
-
- // RPC handle.
- unsigned int rpc_uid{UINT_MAX};
-
- // Last UID used when calling this RPC (primarily for client use). This
- // is incremented for each call.
- unsigned int rpc_call_uid{0};
- };
-
- using EntriesMap = wpi::StringMap<Entry*>;
- using IdMap = std::vector<Entry*>;
- using LocalMap = std::vector<std::unique_ptr<Entry>>;
- using RpcIdPair = std::pair<unsigned int, unsigned int>;
- using RpcResultMap = wpi::DenseMap<RpcIdPair, std::string>;
- using RpcBlockingCallSet = wpi::SmallSet<RpcIdPair, 12>;
-
- mutable wpi::mutex m_mutex;
- EntriesMap m_entries;
- IdMap m_idmap;
- LocalMap m_localmap;
- RpcResultMap m_rpc_results;
- RpcBlockingCallSet m_rpc_blocking_calls;
- // If any persistent values have changed
- mutable bool m_persistent_dirty = false;
-
- // condition variable and termination flag for blocking on a RPC result
- std::atomic_bool m_terminating;
- wpi::condition_variable m_rpc_results_cond;
-
- // configured by dispatcher at startup
- IDispatcher* m_dispatcher = nullptr;
- bool m_server = true;
-
- IEntryNotifier& m_notifier;
- IRpcServer& m_rpc_server;
- wpi::Logger& m_logger;
-
- void ProcessIncomingEntryAssign(std::shared_ptr<Message> msg,
- INetworkConnection* conn);
- void ProcessIncomingEntryUpdate(std::shared_ptr<Message> msg,
- INetworkConnection* conn);
- void ProcessIncomingFlagsUpdate(std::shared_ptr<Message> msg,
- INetworkConnection* conn);
- void ProcessIncomingEntryDelete(std::shared_ptr<Message> msg,
- INetworkConnection* conn);
- void ProcessIncomingClearEntries(std::shared_ptr<Message> msg,
- INetworkConnection* conn);
- void ProcessIncomingExecuteRpc(std::shared_ptr<Message> msg,
- INetworkConnection* conn,
- std::weak_ptr<INetworkConnection> conn_weak);
- void ProcessIncomingRpcResponse(std::shared_ptr<Message> msg,
- INetworkConnection* conn);
-
- bool GetPersistentEntries(
- bool periodic,
- std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
- const;
- bool GetEntries(std::string_view prefix,
- std::vector<std::pair<std::string, std::shared_ptr<Value>>>*
- entries) const;
- void SetEntryValueImpl(Entry* entry, std::shared_ptr<Value> value,
- std::unique_lock<wpi::mutex>& lock, bool local);
- void SetEntryFlagsImpl(Entry* entry, unsigned int flags,
- std::unique_lock<wpi::mutex>& lock, bool local);
- void DeleteEntryImpl(Entry* entry, std::unique_lock<wpi::mutex>& lock,
- bool local);
-
- // Must be called with m_mutex held
- template <typename F>
- void DeleteAllEntriesImpl(bool local, F should_delete);
- void DeleteAllEntriesImpl(bool local);
- Entry* GetOrNew(std::string_view name);
-};
-
-} // namespace nt
-
-#endif // NTCORE_STORAGE_H_
diff --git a/ntcore/src/main/native/cpp/Storage_load.cpp b/ntcore/src/main/native/cpp/Storage_load.cpp
deleted file mode 100644
index 98046e7..0000000
--- a/ntcore/src/main/native/cpp/Storage_load.cpp
+++ /dev/null
@@ -1,484 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include <cctype>
-#include <string>
-#include <utility>
-
-#include <wpi/Base64.h>
-#include <wpi/SmallString.h>
-#include <wpi/StringExtras.h>
-#include <wpi/raw_istream.h>
-
-#include "IDispatcher.h"
-#include "IEntryNotifier.h"
-#include "Storage.h"
-
-using namespace nt;
-
-namespace {
-
-class LoadPersistentImpl {
- public:
- using Entry = std::pair<std::string, std::shared_ptr<Value>>;
- using WarnFunc = std::function<void(size_t, const char*)>;
-
- LoadPersistentImpl(wpi::raw_istream& is, WarnFunc warn)
- : m_is(is), m_warn(std::move(warn)) {}
-
- bool Load(std::string_view prefix, std::vector<Entry>* entries);
-
- private:
- bool ReadLine();
- bool ReadHeader();
- NT_Type ReadType();
- std::string_view ReadName(wpi::SmallVectorImpl<char>& buf);
- std::shared_ptr<Value> ReadValue(NT_Type type);
- std::shared_ptr<Value> ReadBooleanValue();
- std::shared_ptr<Value> ReadDoubleValue();
- std::shared_ptr<Value> ReadStringValue();
- std::shared_ptr<Value> ReadRawValue();
- std::shared_ptr<Value> ReadBooleanArrayValue();
- std::shared_ptr<Value> ReadDoubleArrayValue();
- std::shared_ptr<Value> ReadStringArrayValue();
-
- void Warn(const char* msg) {
- if (m_warn) {
- m_warn(m_line_num, msg);
- }
- }
-
- wpi::raw_istream& m_is;
- WarnFunc m_warn;
-
- std::string_view m_line;
- wpi::SmallString<128> m_line_buf;
- size_t m_line_num = 0;
-
- std::vector<int> m_buf_boolean_array;
- std::vector<double> m_buf_double_array;
- std::vector<std::string> m_buf_string_array;
-};
-
-} // namespace
-
-/* Extracts an escaped string token. Does not unescape the string.
- * If a string cannot be matched, an empty string is returned.
- * If the string is unterminated, an empty tail string is returned.
- * The returned token includes the starting and trailing quotes (unless the
- * string is unterminated).
- * Returns a pair containing the extracted token (if any) and the remaining
- * tail string.
- */
-static std::pair<std::string_view, std::string_view> ReadStringToken(
- std::string_view source) {
- // Match opening quote
- if (source.empty() || source.front() != '"') {
- return {{}, source};
- }
-
- // Scan for ending double quote, checking for escaped as we go.
- size_t size = source.size();
- size_t pos;
- for (pos = 1; pos < size; ++pos) {
- if (source[pos] == '"' && source[pos - 1] != '\\') {
- ++pos; // we want to include the trailing quote in the result
- break;
- }
- }
- return {wpi::slice(source, 0, pos), wpi::substr(source, pos)};
-}
-
-static int fromxdigit(char ch) {
- if (ch >= 'a' && ch <= 'f') {
- return (ch - 'a' + 10);
- } else if (ch >= 'A' && ch <= 'F') {
- return (ch - 'A' + 10);
- } else {
- return ch - '0';
- }
-}
-
-static std::string_view UnescapeString(std::string_view source,
- wpi::SmallVectorImpl<char>& buf) {
- assert(source.size() >= 2 && source.front() == '"' && source.back() == '"');
- buf.clear();
- buf.reserve(source.size() - 2);
- for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) {
- if (*s != '\\') {
- buf.push_back(*s);
- continue;
- }
- switch (*++s) {
- case 't':
- buf.push_back('\t');
- break;
- case 'n':
- buf.push_back('\n');
- break;
- case 'x': {
- if (!isxdigit(*(s + 1))) {
- buf.push_back('x'); // treat it like a unknown escape
- break;
- }
- int ch = fromxdigit(*++s);
- if (std::isxdigit(*(s + 1))) {
- ch <<= 4;
- ch |= fromxdigit(*++s);
- }
- buf.push_back(static_cast<char>(ch));
- break;
- }
- default:
- buf.push_back(*s);
- break;
- }
- }
- return {buf.data(), buf.size()};
-}
-
-bool LoadPersistentImpl::Load(std::string_view prefix,
- std::vector<Entry>* entries) {
- if (!ReadHeader()) {
- return false; // header
- }
-
- while (ReadLine()) {
- // type
- NT_Type type = ReadType();
- if (type == NT_UNASSIGNED) {
- Warn("unrecognized type");
- continue;
- }
-
- // name
- wpi::SmallString<128> buf;
- std::string_view name = ReadName(buf);
- if (name.empty() || !wpi::starts_with(name, prefix)) {
- continue;
- }
-
- // =
- m_line = wpi::ltrim(m_line, " \t");
- if (m_line.empty() || m_line.front() != '=') {
- Warn("expected = after name");
- continue;
- }
- m_line.remove_prefix(1);
- m_line = wpi::ltrim(m_line, " \t");
-
- // value
- auto value = ReadValue(type);
-
- // move to entries
- if (value) {
- entries->emplace_back(name, std::move(value));
- }
- }
- return true;
-}
-
-bool LoadPersistentImpl::ReadLine() {
- // ignore blank lines and lines that start with ; or # (comments)
- while (!m_is.has_error()) {
- ++m_line_num;
- m_line = wpi::trim(m_is.getline(m_line_buf, INT_MAX));
- if (!m_line.empty() && m_line.front() != ';' && m_line.front() != '#') {
- return true;
- }
- }
- return false;
-}
-
-bool LoadPersistentImpl::ReadHeader() {
- // header
- if (!ReadLine() || m_line != "[NetworkTables Storage 3.0]") {
- Warn("header line mismatch, ignoring rest of file");
- return false;
- }
- return true;
-}
-
-NT_Type LoadPersistentImpl::ReadType() {
- std::string_view tok;
- std::tie(tok, m_line) = wpi::split(m_line, ' ');
- if (tok == "boolean") {
- return NT_BOOLEAN;
- } else if (tok == "double") {
- return NT_DOUBLE;
- } else if (tok == "string") {
- return NT_STRING;
- } else if (tok == "raw") {
- return NT_RAW;
- } else if (tok == "array") {
- std::string_view array_tok;
- std::tie(array_tok, m_line) = wpi::split(m_line, ' ');
- if (array_tok == "boolean") {
- return NT_BOOLEAN_ARRAY;
- } else if (array_tok == "double") {
- return NT_DOUBLE_ARRAY;
- } else if (array_tok == "string") {
- return NT_STRING_ARRAY;
- }
- }
- return NT_UNASSIGNED;
-}
-
-std::string_view LoadPersistentImpl::ReadName(wpi::SmallVectorImpl<char>& buf) {
- std::string_view tok;
- std::tie(tok, m_line) = ReadStringToken(m_line);
- if (tok.empty()) {
- Warn("missing name");
- return {};
- }
- if (tok.back() != '"') {
- Warn("unterminated name string");
- return {};
- }
- return UnescapeString(tok, buf);
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadValue(NT_Type type) {
- switch (type) {
- case NT_BOOLEAN:
- return ReadBooleanValue();
- case NT_DOUBLE:
- return ReadDoubleValue();
- case NT_STRING:
- return ReadStringValue();
- case NT_RAW:
- return ReadRawValue();
- case NT_BOOLEAN_ARRAY:
- return ReadBooleanArrayValue();
- case NT_DOUBLE_ARRAY:
- return ReadDoubleArrayValue();
- case NT_STRING_ARRAY:
- return ReadStringArrayValue();
- default:
- return nullptr;
- }
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadBooleanValue() {
- // only true or false is accepted
- if (m_line == "true") {
- return Value::MakeBoolean(true);
- }
- if (m_line == "false") {
- return Value::MakeBoolean(false);
- }
- Warn("unrecognized boolean value, not 'true' or 'false'");
- return nullptr;
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadDoubleValue() {
- // need to convert to null-terminated string for std::strtod()
- wpi::SmallString<64> buf{m_line};
- char* end;
- double v = std::strtod(buf.c_str(), &end);
- if (*end != '\0') {
- Warn("invalid double value");
- return nullptr;
- }
- return Value::MakeDouble(v);
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadStringValue() {
- std::string_view tok;
- std::tie(tok, m_line) = ReadStringToken(m_line);
- if (tok.empty()) {
- Warn("missing string value");
- return nullptr;
- }
- if (tok.back() != '"') {
- Warn("unterminated string value");
- return nullptr;
- }
- wpi::SmallString<128> buf;
- return Value::MakeString(UnescapeString(tok, buf));
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadRawValue() {
- wpi::SmallString<128> buf;
- size_t nr;
- return Value::MakeRaw(wpi::Base64Decode(m_line, &nr, buf));
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadBooleanArrayValue() {
- m_buf_boolean_array.clear();
- while (!m_line.empty()) {
- std::string_view tok;
- std::tie(tok, m_line) = wpi::split(m_line, ',');
- tok = wpi::trim(tok, " \t");
- if (tok == "true") {
- m_buf_boolean_array.push_back(1);
- } else if (tok == "false") {
- m_buf_boolean_array.push_back(0);
- } else {
- Warn("unrecognized boolean value, not 'true' or 'false'");
- return nullptr;
- }
- }
- return Value::MakeBooleanArray(std::move(m_buf_boolean_array));
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadDoubleArrayValue() {
- m_buf_double_array.clear();
- while (!m_line.empty()) {
- std::string_view tok;
- std::tie(tok, m_line) = wpi::split(m_line, ',');
- tok = wpi::trim(tok, " \t");
- // need to convert to null-terminated string for std::strtod()
- wpi::SmallString<64> buf{tok};
- char* end;
- double v = std::strtod(buf.c_str(), &end);
- if (*end != '\0') {
- Warn("invalid double value");
- return nullptr;
- }
- m_buf_double_array.push_back(v);
- }
-
- return Value::MakeDoubleArray(std::move(m_buf_double_array));
-}
-
-std::shared_ptr<Value> LoadPersistentImpl::ReadStringArrayValue() {
- m_buf_string_array.clear();
- while (!m_line.empty()) {
- std::string_view tok;
- std::tie(tok, m_line) = ReadStringToken(m_line);
- if (tok.empty()) {
- Warn("missing string value");
- return nullptr;
- }
- if (tok.back() != '"') {
- Warn("unterminated string value");
- return nullptr;
- }
-
- wpi::SmallString<128> buf;
- m_buf_string_array.emplace_back(UnescapeString(tok, buf));
-
- m_line = wpi::ltrim(m_line, " \t");
- if (m_line.empty()) {
- break;
- }
- if (m_line.front() != ',') {
- Warn("expected comma between strings");
- return nullptr;
- }
- m_line.remove_prefix(1);
- m_line = wpi::ltrim(m_line, " \t");
- }
-
- return Value::MakeStringArray(std::move(m_buf_string_array));
-}
-
-bool Storage::LoadEntries(
- wpi::raw_istream& is, std::string_view prefix, bool persistent,
- std::function<void(size_t line, const char* msg)> warn) {
- // entries to add
- std::vector<LoadPersistentImpl::Entry> entries;
-
- // load file
- if (!LoadPersistentImpl(is, warn).Load(prefix, &entries)) {
- return false;
- }
-
- // copy values into storage as quickly as possible so lock isn't held
- std::vector<std::shared_ptr<Message>> msgs;
- std::unique_lock lock(m_mutex);
- for (auto& i : entries) {
- Entry* entry = GetOrNew(i.first);
- auto old_value = entry->value;
- entry->value = i.second;
- bool was_persist = entry->IsPersistent();
- if (!was_persist && persistent) {
- entry->flags |= NT_PERSISTENT;
- }
-
- // if we're the server, assign an id if it doesn't have one
- if (m_server && entry->id == 0xffff) {
- unsigned int id = m_idmap.size();
- entry->id = id;
- m_idmap.push_back(entry);
- }
-
- // notify (for local listeners)
- if (m_notifier.local_notifiers()) {
- if (!old_value) {
- m_notifier.NotifyEntry(entry->local_id, i.first, i.second,
- NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
- } else if (*old_value != *i.second) {
- unsigned int notify_flags = NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL;
- if (!was_persist && persistent) {
- notify_flags |= NT_NOTIFY_FLAGS;
- }
- m_notifier.NotifyEntry(entry->local_id, i.first, i.second,
- notify_flags);
- } else if (!was_persist && persistent) {
- m_notifier.NotifyEntry(entry->local_id, i.first, i.second,
- NT_NOTIFY_FLAGS | NT_NOTIFY_LOCAL);
- }
- }
-
- if (!m_dispatcher) {
- continue; // shortcut
- }
- ++entry->seq_num;
-
- // put on update queue
- if (!old_value || old_value->type() != i.second->type()) {
- msgs.emplace_back(Message::EntryAssign(
- i.first, entry->id, entry->seq_num.value(), i.second, entry->flags));
- } else if (entry->id != 0xffff) {
- // don't send an update if we don't have an assigned id yet
- if (*old_value != *i.second) {
- msgs.emplace_back(
- Message::EntryUpdate(entry->id, entry->seq_num.value(), i.second));
- }
- if (!was_persist) {
- msgs.emplace_back(Message::FlagsUpdate(entry->id, entry->flags));
- }
- }
- }
-
- if (m_dispatcher) {
- auto dispatcher = m_dispatcher;
- lock.unlock();
- for (auto& msg : msgs) {
- dispatcher->QueueOutgoing(std::move(msg), nullptr, nullptr);
- }
- }
-
- return true;
-}
-
-const char* Storage::LoadPersistent(
- std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn) {
- std::error_code ec;
- wpi::raw_fd_istream is(filename, ec);
- if (ec.value() != 0) {
- return "could not open file";
- }
- if (!LoadEntries(is, "", true, warn)) {
- return "error reading file";
- }
- return nullptr;
-}
-
-const char* Storage::LoadEntries(
- std::string_view filename, std::string_view prefix,
- std::function<void(size_t line, const char* msg)> warn) {
- std::error_code ec;
- wpi::raw_fd_istream is(filename, ec);
- if (ec.value() != 0) {
- return "could not open file";
- }
- if (!LoadEntries(is, prefix, false, warn)) {
- return "error reading file";
- }
- return nullptr;
-}
diff --git a/ntcore/src/main/native/cpp/Storage_save.cpp b/ntcore/src/main/native/cpp/Storage_save.cpp
deleted file mode 100644
index 6d5db9f..0000000
--- a/ntcore/src/main/native/cpp/Storage_save.cpp
+++ /dev/null
@@ -1,283 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include <cctype>
-#include <string>
-
-#include <fmt/format.h>
-#include <wpi/Base64.h>
-#include <wpi/SmallString.h>
-#include <wpi/StringExtras.h>
-#include <wpi/fs.h>
-#include <wpi/raw_ostream.h>
-
-#include "Log.h"
-#include "Storage.h"
-
-using namespace nt;
-
-namespace {
-
-class SavePersistentImpl {
- public:
- using Entry = std::pair<std::string, std::shared_ptr<Value>>;
-
- explicit SavePersistentImpl(wpi::raw_ostream& os) : m_os(os) {}
-
- void Save(wpi::span<const Entry> entries);
-
- private:
- void WriteString(std::string_view str);
- void WriteHeader();
- void WriteEntries(wpi::span<const Entry> entries);
- void WriteEntry(std::string_view name, const Value& value);
- bool WriteType(NT_Type type);
- void WriteValue(const Value& value);
-
- wpi::raw_ostream& m_os;
-};
-
-} // namespace
-
-/* Escapes and writes a string, including start and end double quotes */
-void SavePersistentImpl::WriteString(std::string_view str) {
- m_os << '"';
- for (auto c : str) {
- switch (c) {
- case '\\':
- m_os << "\\\\";
- break;
- case '\t':
- m_os << "\\t";
- break;
- case '\n':
- m_os << "\\n";
- break;
- case '"':
- m_os << "\\\"";
- break;
- default:
- if (std::isprint(c) && c != '=') {
- m_os << c;
- break;
- }
-
- // Write out the escaped representation.
- m_os << "\\x";
- m_os << wpi::hexdigit((c >> 4) & 0xF);
- m_os << wpi::hexdigit((c >> 0) & 0xF);
- }
- }
- m_os << '"';
-}
-
-void SavePersistentImpl::Save(wpi::span<const Entry> entries) {
- WriteHeader();
- WriteEntries(entries);
-}
-
-void SavePersistentImpl::WriteHeader() {
- m_os << "[NetworkTables Storage 3.0]\n";
-}
-
-void SavePersistentImpl::WriteEntries(wpi::span<const Entry> entries) {
- for (auto& i : entries) {
- if (!i.second) {
- continue;
- }
- WriteEntry(i.first, *i.second);
- }
-}
-
-void SavePersistentImpl::WriteEntry(std::string_view name, const Value& value) {
- if (!WriteType(value.type())) {
- return; // type
- }
- WriteString(name); // name
- m_os << '='; // '='
- WriteValue(value); // value
- m_os << '\n'; // eol
-}
-
-bool SavePersistentImpl::WriteType(NT_Type type) {
- switch (type) {
- case NT_BOOLEAN:
- m_os << "boolean ";
- break;
- case NT_DOUBLE:
- m_os << "double ";
- break;
- case NT_STRING:
- m_os << "string ";
- break;
- case NT_RAW:
- m_os << "raw ";
- break;
- case NT_BOOLEAN_ARRAY:
- m_os << "array boolean ";
- break;
- case NT_DOUBLE_ARRAY:
- m_os << "array double ";
- break;
- case NT_STRING_ARRAY:
- m_os << "array string ";
- break;
- default:
- return false;
- }
- return true;
-}
-
-void SavePersistentImpl::WriteValue(const Value& value) {
- switch (value.type()) {
- case NT_BOOLEAN:
- m_os << (value.GetBoolean() ? "true" : "false");
- break;
- case NT_DOUBLE:
- m_os << fmt::format("{:g}", value.GetDouble());
- break;
- case NT_STRING:
- WriteString(value.GetString());
- break;
- case NT_RAW: {
- wpi::Base64Encode(m_os, value.GetRaw());
- break;
- }
- case NT_BOOLEAN_ARRAY: {
- bool first = true;
- for (auto elem : value.GetBooleanArray()) {
- if (!first) {
- m_os << ',';
- }
- first = false;
- m_os << (elem ? "true" : "false");
- }
- break;
- }
- case NT_DOUBLE_ARRAY: {
- bool first = true;
- for (auto elem : value.GetDoubleArray()) {
- if (!first) {
- m_os << ',';
- }
- first = false;
- m_os << fmt::format("{:g}", elem);
- }
- break;
- }
- case NT_STRING_ARRAY: {
- bool first = true;
- for (auto& elem : value.GetStringArray()) {
- if (!first) {
- m_os << ',';
- }
- first = false;
- WriteString(elem);
- }
- break;
- }
- default:
- break;
- }
-}
-
-void Storage::SavePersistent(wpi::raw_ostream& os, bool periodic) const {
- std::vector<SavePersistentImpl::Entry> entries;
- if (!GetPersistentEntries(periodic, &entries)) {
- return;
- }
- SavePersistentImpl(os).Save(entries);
-}
-
-const char* Storage::SavePersistent(std::string_view filename,
- bool periodic) const {
- std::string fn{filename};
- auto tmp = fmt::format("{}.tmp", filename);
- auto bak = fmt::format("{}.bak", filename);
-
- // Get entries before creating file
- std::vector<SavePersistentImpl::Entry> entries;
- if (!GetPersistentEntries(periodic, &entries)) {
- return nullptr;
- }
-
- const char* err = nullptr;
-
- // start by writing to temporary file
- std::error_code ec;
- wpi::raw_fd_ostream os(tmp, ec, fs::F_Text);
- if (ec.value() != 0) {
- err = "could not open file";
- goto done;
- }
- DEBUG0("saving persistent file '{}'", filename);
- SavePersistentImpl(os).Save(entries);
- os.close();
- if (os.has_error()) {
- std::remove(tmp.c_str());
- err = "error saving file";
- goto done;
- }
-
- // Safely move to real file. We ignore any failures related to the backup.
- std::remove(bak.c_str());
- std::rename(fn.c_str(), bak.c_str());
- if (std::rename(tmp.c_str(), fn.c_str()) != 0) {
- std::rename(bak.c_str(), fn.c_str()); // attempt to restore backup
- err = "could not rename temp file to real file";
- goto done;
- }
-
-done:
- // try again if there was an error
- if (err && periodic) {
- m_persistent_dirty = true;
- }
- return err;
-}
-
-void Storage::SaveEntries(wpi::raw_ostream& os, std::string_view prefix) const {
- std::vector<SavePersistentImpl::Entry> entries;
- if (!GetEntries(prefix, &entries)) {
- return;
- }
- SavePersistentImpl(os).Save(entries);
-}
-
-const char* Storage::SaveEntries(std::string_view filename,
- std::string_view prefix) const {
- std::string fn{filename};
- auto tmp = fmt::format("{}.tmp", filename);
- auto bak = fmt::format("{}.bak", filename);
-
- // Get entries before creating file
- std::vector<SavePersistentImpl::Entry> entries;
- if (!GetEntries(prefix, &entries)) {
- return nullptr;
- }
-
- // start by writing to temporary file
- std::error_code ec;
- wpi::raw_fd_ostream os(tmp, ec, fs::F_Text);
- if (ec.value() != 0) {
- return "could not open file";
- }
- DEBUG0("saving file '{}'", filename);
- SavePersistentImpl(os).Save(entries);
- os.close();
- if (os.has_error()) {
- std::remove(tmp.c_str());
- return "error saving file";
- }
-
- // Safely move to real file. We ignore any failures related to the backup.
- std::remove(bak.c_str());
- std::rename(fn.c_str(), bak.c_str());
- if (std::rename(tmp.c_str(), fn.c_str()) != 0) {
- std::rename(bak.c_str(), fn.c_str()); // attempt to restore backup
- return "could not rename temp file to real file";
- }
-
- return nullptr;
-}
diff --git a/ntcore/src/main/native/cpp/Types_internal.cpp b/ntcore/src/main/native/cpp/Types_internal.cpp
new file mode 100644
index 0000000..d6de540
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Types_internal.cpp
@@ -0,0 +1,83 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "Types_internal.h"
+
+std::string_view nt::TypeToString(NT_Type type) {
+ switch (type) {
+ case NT_BOOLEAN:
+ return "boolean";
+ case NT_DOUBLE:
+ return "double";
+ case NT_STRING:
+ return "string";
+ case NT_BOOLEAN_ARRAY:
+ return "boolean[]";
+ case NT_DOUBLE_ARRAY:
+ return "double[]";
+ case NT_STRING_ARRAY:
+ return "string[]";
+ case NT_RPC:
+ return "rpc";
+ case NT_INTEGER:
+ return "int";
+ case NT_FLOAT:
+ return "float";
+ case NT_INTEGER_ARRAY:
+ return "int[]";
+ case NT_FLOAT_ARRAY:
+ return "float";
+ default:
+ return "raw";
+ }
+}
+
+NT_Type nt::StringToType(std::string_view typeStr) {
+ if (typeStr == "boolean") {
+ return NT_BOOLEAN;
+ } else if (typeStr == "double") {
+ return NT_DOUBLE;
+ } else if (typeStr == "string" || typeStr == "json") {
+ return NT_STRING;
+ } else if (typeStr == "boolean[]") {
+ return NT_BOOLEAN_ARRAY;
+ } else if (typeStr == "double[]") {
+ return NT_DOUBLE_ARRAY;
+ } else if (typeStr == "string[]") {
+ return NT_STRING_ARRAY;
+ } else if (typeStr == "rpc") {
+ return NT_RPC;
+ } else if (typeStr == "int") {
+ return NT_INTEGER;
+ } else if (typeStr == "float") {
+ return NT_FLOAT;
+ } else if (typeStr == "int[]") {
+ return NT_INTEGER_ARRAY;
+ } else if (typeStr == "float[]") {
+ return NT_FLOAT_ARRAY;
+ } else {
+ return NT_RAW;
+ }
+}
+
+NT_Type nt::StringToType3(std::string_view typeStr) {
+ if (typeStr == "boolean") {
+ return NT_BOOLEAN;
+ } else if (typeStr == "double" || typeStr == "int" || typeStr == "float") {
+ return NT_DOUBLE;
+ } else if (typeStr == "string" || typeStr == "json") {
+ return NT_STRING;
+ } else if (typeStr == "boolean[]") {
+ return NT_BOOLEAN_ARRAY;
+ } else if (typeStr == "double[]" || typeStr == "int[]" ||
+ typeStr == "float[]") {
+ return NT_DOUBLE_ARRAY;
+ } else if (typeStr == "string[]") {
+ return NT_STRING_ARRAY;
+ } else if (typeStr == "rpc") {
+ return NT_RPC;
+ } else {
+ return NT_RAW;
+ }
+}
diff --git a/ntcore/src/main/native/cpp/Types_internal.h b/ntcore/src/main/native/cpp/Types_internal.h
new file mode 100644
index 0000000..0919641
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Types_internal.h
@@ -0,0 +1,30 @@
+// 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 <string_view>
+
+#include "ntcore_c.h"
+
+namespace nt {
+
+std::string_view TypeToString(NT_Type type);
+NT_Type StringToType(std::string_view typeStr);
+NT_Type StringToType3(std::string_view typeStr);
+
+constexpr bool IsNumeric(NT_Type type) {
+ return (type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE)) != 0;
+}
+
+constexpr bool IsNumericArray(NT_Type type) {
+ return (type & (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY)) != 0;
+}
+
+constexpr bool IsNumericCompatible(NT_Type a, NT_Type b) {
+ return (IsNumeric(a) && IsNumeric(b)) ||
+ (IsNumericArray(a) && IsNumericArray(b));
+}
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/Value.cpp b/ntcore/src/main/native/cpp/Value.cpp
index e0d65c9..240447f 100644
--- a/ntcore/src/main/native/cpp/Value.cpp
+++ b/ntcore/src/main/native/cpp/Value.cpp
@@ -5,29 +5,63 @@
#include <stdint.h>
#include <cstring>
+#include <span>
#include <wpi/MemAlloc.h>
#include <wpi/timestamp.h>
#include "Value_internal.h"
#include "networktables/NetworkTableValue.h"
+#include "ntcore_cpp.h"
using namespace nt;
-Value::Value() {
- m_val.type = NT_UNASSIGNED;
- m_val.last_change = wpi::Now();
+namespace {
+struct StringArrayStorage {
+ explicit StringArrayStorage(std::span<const std::string> value)
+ : strings{value.begin(), value.end()} {
+ InitNtStrings();
+ }
+ explicit StringArrayStorage(std::vector<std::string>&& value)
+ : strings{std::move(value)} {
+ InitNtStrings();
+ }
+ void InitNtStrings();
+
+ std::vector<std::string> strings;
+ std::vector<NT_String> ntStrings;
+};
+} // namespace
+
+void StringArrayStorage::InitNtStrings() {
+ // point NT_String's to the contents in the vector.
+ ntStrings.reserve(strings.size());
+ for (const auto& str : strings) {
+ ntStrings.emplace_back(
+ NT_String{const_cast<char*>(str.c_str()), str.size()});
+ }
}
-Value::Value(NT_Type type, uint64_t time, const private_init&) {
+Value::Value() {
+ m_val.type = NT_UNASSIGNED;
+ m_val.last_change = 0;
+ m_val.server_time = 0;
+}
+
+Value::Value(NT_Type type, int64_t time, const private_init&)
+ : Value{type, time == 0 ? nt::Now() : time, 1, private_init{}} {}
+
+Value::Value(NT_Type type, int64_t time, int64_t serverTime,
+ const private_init&) {
m_val.type = type;
- if (time == 0) {
- m_val.last_change = wpi::Now();
- } else {
- m_val.last_change = time;
- }
+ m_val.last_change = time;
+ m_val.server_time = serverTime;
if (m_val.type == NT_BOOLEAN_ARRAY) {
m_val.data.arr_boolean.arr = nullptr;
+ } else if (m_val.type == NT_INTEGER_ARRAY) {
+ m_val.data.arr_int.arr = nullptr;
+ } else if (m_val.type == NT_FLOAT_ARRAY) {
+ m_val.data.arr_float.arr = nullptr;
} else if (m_val.type == NT_DOUBLE_ARRAY) {
m_val.data.arr_double.arr = nullptr;
} else if (m_val.type == NT_STRING_ARRAY) {
@@ -35,93 +69,121 @@
}
}
-Value::~Value() {
- if (m_val.type == NT_BOOLEAN_ARRAY) {
- delete[] m_val.data.arr_boolean.arr;
- } else if (m_val.type == NT_DOUBLE_ARRAY) {
- delete[] m_val.data.arr_double.arr;
- } else if (m_val.type == NT_STRING_ARRAY) {
- delete[] m_val.data.arr_string.arr;
- }
-}
-
-std::shared_ptr<Value> Value::MakeBooleanArray(wpi::span<const bool> value,
- uint64_t time) {
- auto val = std::make_shared<Value>(NT_BOOLEAN_ARRAY, time, private_init());
- val->m_val.data.arr_boolean.arr = new int[value.size()];
- val->m_val.data.arr_boolean.size = value.size();
- std::copy(value.begin(), value.end(), val->m_val.data.arr_boolean.arr);
+Value Value::MakeBooleanArray(std::span<const bool> value, int64_t time) {
+ Value val{NT_BOOLEAN_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<int>>(value.begin(), value.end());
+ // data->reserve(value.size());
+ // std::copy(value.begin(), value.end(), *data);
+ val.m_val.data.arr_boolean.arr = data->data();
+ val.m_val.data.arr_boolean.size = data->size();
+ val.m_storage = std::move(data);
return val;
}
-std::shared_ptr<Value> Value::MakeBooleanArray(wpi::span<const int> value,
- uint64_t time) {
- auto val = std::make_shared<Value>(NT_BOOLEAN_ARRAY, time, private_init());
- val->m_val.data.arr_boolean.arr = new int[value.size()];
- val->m_val.data.arr_boolean.size = value.size();
- std::copy(value.begin(), value.end(), val->m_val.data.arr_boolean.arr);
+Value Value::MakeBooleanArray(std::span<const int> value, int64_t time) {
+ Value val{NT_BOOLEAN_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<int>>(value.begin(), value.end());
+ val.m_val.data.arr_boolean.arr = data->data();
+ val.m_val.data.arr_boolean.size = data->size();
+ val.m_storage = std::move(data);
return val;
}
-std::shared_ptr<Value> Value::MakeDoubleArray(wpi::span<const double> value,
- uint64_t time) {
- auto val = std::make_shared<Value>(NT_DOUBLE_ARRAY, time, private_init());
- val->m_val.data.arr_double.arr = new double[value.size()];
- val->m_val.data.arr_double.size = value.size();
- std::copy(value.begin(), value.end(), val->m_val.data.arr_double.arr);
+Value Value::MakeBooleanArray(std::vector<int>&& value, int64_t time) {
+ Value val{NT_BOOLEAN_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<int>>(std::move(value));
+ val.m_val.data.arr_boolean.arr = data->data();
+ val.m_val.data.arr_boolean.size = data->size();
+ val.m_storage = std::move(data);
return val;
}
-std::shared_ptr<Value> Value::MakeStringArray(
- wpi::span<const std::string> value, uint64_t time) {
- auto val = std::make_shared<Value>(NT_STRING_ARRAY, time, private_init());
- val->m_string_array.assign(value.begin(), value.end());
- // point NT_Value to the contents in the vector.
- val->m_val.data.arr_string.arr = new NT_String[value.size()];
- val->m_val.data.arr_string.size = val->m_string_array.size();
- for (size_t i = 0; i < value.size(); ++i) {
- val->m_val.data.arr_string.arr[i].str = const_cast<char*>(value[i].c_str());
- val->m_val.data.arr_string.arr[i].len = value[i].size();
- }
+Value Value::MakeIntegerArray(std::span<const int64_t> value, int64_t time) {
+ Value val{NT_INTEGER_ARRAY, time, private_init{}};
+ auto data =
+ std::make_shared<std::vector<int64_t>>(value.begin(), value.end());
+ val.m_val.data.arr_int.arr = data->data();
+ val.m_val.data.arr_int.size = data->size();
+ val.m_storage = std::move(data);
return val;
}
-std::shared_ptr<Value> Value::MakeStringArray(std::vector<std::string>&& value,
- uint64_t time) {
- auto val = std::make_shared<Value>(NT_STRING_ARRAY, time, private_init());
- val->m_string_array = std::move(value);
- value.clear();
- // point NT_Value to the contents in the vector.
- val->m_val.data.arr_string.arr = new NT_String[val->m_string_array.size()];
- val->m_val.data.arr_string.size = val->m_string_array.size();
- for (size_t i = 0; i < val->m_string_array.size(); ++i) {
- val->m_val.data.arr_string.arr[i].str =
- const_cast<char*>(val->m_string_array[i].c_str());
- val->m_val.data.arr_string.arr[i].len = val->m_string_array[i].size();
- }
+Value Value::MakeIntegerArray(std::vector<int64_t>&& value, int64_t time) {
+ Value val{NT_INTEGER_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<int64_t>>(std::move(value));
+ val.m_val.data.arr_int.arr = data->data();
+ val.m_val.data.arr_int.size = data->size();
+ val.m_storage = std::move(data);
+ return val;
+}
+
+Value Value::MakeFloatArray(std::span<const float> value, int64_t time) {
+ Value val{NT_FLOAT_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<float>>(value.begin(), value.end());
+ val.m_val.data.arr_float.arr = data->data();
+ val.m_val.data.arr_float.size = data->size();
+ val.m_storage = std::move(data);
+ return val;
+}
+
+Value Value::MakeFloatArray(std::vector<float>&& value, int64_t time) {
+ Value val{NT_FLOAT_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<float>>(std::move(value));
+ val.m_val.data.arr_float.arr = data->data();
+ val.m_val.data.arr_float.size = data->size();
+ val.m_storage = std::move(data);
+ return val;
+}
+
+Value Value::MakeDoubleArray(std::span<const double> value, int64_t time) {
+ Value val{NT_DOUBLE_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<double>>(value.begin(), value.end());
+ val.m_val.data.arr_double.arr = data->data();
+ val.m_val.data.arr_double.size = data->size();
+ val.m_storage = std::move(data);
+ return val;
+}
+
+Value Value::MakeDoubleArray(std::vector<double>&& value, int64_t time) {
+ Value val{NT_DOUBLE_ARRAY, time, private_init{}};
+ auto data = std::make_shared<std::vector<double>>(std::move(value));
+ val.m_val.data.arr_double.arr = data->data();
+ val.m_val.data.arr_double.size = data->size();
+ val.m_storage = std::move(data);
+ return val;
+}
+
+Value Value::MakeStringArray(std::span<const std::string> value, int64_t time) {
+ Value val{NT_STRING_ARRAY, time, private_init{}};
+ auto data = std::make_shared<StringArrayStorage>(value);
+ val.m_val.data.arr_string.arr = data->ntStrings.data();
+ val.m_val.data.arr_string.size = data->ntStrings.size();
+ val.m_storage = std::move(data);
+ return val;
+}
+
+Value Value::MakeStringArray(std::vector<std::string>&& value, int64_t time) {
+ Value val{NT_STRING_ARRAY, time, private_init{}};
+ auto data = std::make_shared<StringArrayStorage>(std::move(value));
+ val.m_val.data.arr_string.arr = data->ntStrings.data();
+ val.m_val.data.arr_string.size = data->ntStrings.size();
+ val.m_storage = std::move(data);
return val;
}
void nt::ConvertToC(const Value& in, NT_Value* out) {
- out->type = NT_UNASSIGNED;
+ *out = in.value();
switch (in.type()) {
- case NT_UNASSIGNED:
- return;
- case NT_BOOLEAN:
- out->data.v_boolean = in.GetBoolean() ? 1 : 0;
- break;
- case NT_DOUBLE:
- out->data.v_double = in.GetDouble();
- break;
case NT_STRING:
ConvertToC(in.GetString(), &out->data.v_string);
break;
- case NT_RAW:
- ConvertToC(in.GetRaw(), &out->data.v_raw);
+ case NT_RAW: {
+ auto v = in.GetRaw();
+ out->data.v_raw.data = static_cast<uint8_t*>(wpi::safe_malloc(v.size()));
+ out->data.v_raw.size = v.size();
+ std::memcpy(out->data.v_raw.data, v.data(), v.size());
break;
- case NT_RPC:
- ConvertToC(in.GetRpc(), &out->data.v_raw);
- break;
+ }
case NT_BOOLEAN_ARRAY: {
auto v = in.GetBooleanArray();
out->data.arr_boolean.arr =
@@ -130,6 +192,22 @@
std::copy(v.begin(), v.end(), out->data.arr_boolean.arr);
break;
}
+ case NT_INTEGER_ARRAY: {
+ auto v = in.GetIntegerArray();
+ out->data.arr_int.arr =
+ static_cast<int64_t*>(wpi::safe_malloc(v.size() * sizeof(int64_t)));
+ out->data.arr_int.size = v.size();
+ std::copy(v.begin(), v.end(), out->data.arr_int.arr);
+ break;
+ }
+ case NT_FLOAT_ARRAY: {
+ auto v = in.GetFloatArray();
+ out->data.arr_float.arr =
+ static_cast<float*>(wpi::safe_malloc(v.size() * sizeof(float)));
+ out->data.arr_float.size = v.size();
+ std::copy(v.begin(), v.end(), out->data.arr_float.arr);
+ break;
+ }
case NT_DOUBLE_ARRAY: {
auto v = in.GetDoubleArray();
out->data.arr_double.arr =
@@ -143,16 +221,21 @@
out->data.arr_string.arr = static_cast<NT_String*>(
wpi::safe_malloc(v.size() * sizeof(NT_String)));
for (size_t i = 0; i < v.size(); ++i) {
- ConvertToC(v[i], &out->data.arr_string.arr[i]);
+ ConvertToC(std::string_view{v[i]}, &out->data.arr_string.arr[i]);
}
out->data.arr_string.size = v.size();
break;
}
default:
- // assert(false && "unknown value type");
- return;
+ break;
}
- out->type = in.type();
+}
+
+size_t nt::ConvertToC(std::string_view in, char** out) {
+ *out = static_cast<char*>(wpi::safe_malloc(in.size() + 1));
+ std::memmove(*out, in.data(), in.size()); // NOLINT
+ (*out)[in.size()] = '\0';
+ return in.size();
}
void nt::ConvertToC(std::string_view in, NT_String* out) {
@@ -162,37 +245,48 @@
out->str[in.size()] = '\0';
}
-std::shared_ptr<Value> nt::ConvertFromC(const NT_Value& value) {
+Value nt::ConvertFromC(const NT_Value& value) {
switch (value.type) {
- case NT_UNASSIGNED:
- return nullptr;
case NT_BOOLEAN:
- return Value::MakeBoolean(value.data.v_boolean != 0);
+ return Value::MakeBoolean(value.data.v_boolean != 0, value.last_change);
+ case NT_INTEGER:
+ return Value::MakeInteger(value.data.v_int, value.last_change);
+ case NT_FLOAT:
+ return Value::MakeFloat(value.data.v_float, value.last_change);
case NT_DOUBLE:
- return Value::MakeDouble(value.data.v_double);
+ return Value::MakeDouble(value.data.v_double, value.last_change);
case NT_STRING:
- return Value::MakeString(ConvertFromC(value.data.v_string));
+ return Value::MakeString(ConvertFromC(value.data.v_string),
+ value.last_change);
case NT_RAW:
- return Value::MakeRaw(ConvertFromC(value.data.v_raw));
- case NT_RPC:
- return Value::MakeRpc(ConvertFromC(value.data.v_raw));
+ return Value::MakeRaw({value.data.v_raw.data, value.data.v_raw.size},
+ value.last_change);
case NT_BOOLEAN_ARRAY:
return Value::MakeBooleanArray(
- wpi::span(value.data.arr_boolean.arr, value.data.arr_boolean.size));
+ std::span(value.data.arr_boolean.arr, value.data.arr_boolean.size),
+ value.last_change);
+ case NT_INTEGER_ARRAY:
+ return Value::MakeIntegerArray(
+ std::span(value.data.arr_int.arr, value.data.arr_int.size),
+ value.last_change);
+ case NT_FLOAT_ARRAY:
+ return Value::MakeFloatArray(
+ std::span(value.data.arr_float.arr, value.data.arr_float.size),
+ value.last_change);
case NT_DOUBLE_ARRAY:
return Value::MakeDoubleArray(
- wpi::span(value.data.arr_double.arr, value.data.arr_double.size));
+ std::span(value.data.arr_double.arr, value.data.arr_double.size),
+ value.last_change);
case NT_STRING_ARRAY: {
std::vector<std::string> v;
v.reserve(value.data.arr_string.size);
for (size_t i = 0; i < value.data.arr_string.size; ++i) {
v.emplace_back(ConvertFromC(value.data.arr_string.arr[i]));
}
- return Value::MakeStringArray(std::move(v));
+ return Value::MakeStringArray(std::move(v), value.last_change);
}
default:
- // assert(false && "unknown value type");
- return nullptr;
+ return {};
}
}
@@ -205,12 +299,20 @@
return true; // XXX: is this better being false instead?
case NT_BOOLEAN:
return lhs.m_val.data.v_boolean == rhs.m_val.data.v_boolean;
+ case NT_INTEGER:
+ return lhs.m_val.data.v_int == rhs.m_val.data.v_int;
+ case NT_FLOAT:
+ return lhs.m_val.data.v_float == rhs.m_val.data.v_float;
case NT_DOUBLE:
return lhs.m_val.data.v_double == rhs.m_val.data.v_double;
case NT_STRING:
+ return lhs.GetString() == rhs.GetString();
case NT_RAW:
- case NT_RPC:
- return lhs.m_string == rhs.m_string;
+ if (lhs.m_val.data.v_raw.size != rhs.m_val.data.v_raw.size) {
+ return false;
+ }
+ return std::memcmp(lhs.m_val.data.v_raw.data, rhs.m_val.data.v_raw.data,
+ lhs.m_val.data.v_raw.size) == 0;
case NT_BOOLEAN_ARRAY:
if (lhs.m_val.data.arr_boolean.size != rhs.m_val.data.arr_boolean.size) {
return false;
@@ -219,6 +321,21 @@
rhs.m_val.data.arr_boolean.arr,
lhs.m_val.data.arr_boolean.size *
sizeof(lhs.m_val.data.arr_boolean.arr[0])) == 0;
+ case NT_INTEGER_ARRAY:
+ if (lhs.m_val.data.arr_int.size != rhs.m_val.data.arr_int.size) {
+ return false;
+ }
+ return std::memcmp(lhs.m_val.data.arr_int.arr, rhs.m_val.data.arr_int.arr,
+ lhs.m_val.data.arr_int.size *
+ sizeof(lhs.m_val.data.arr_int.arr[0])) == 0;
+ case NT_FLOAT_ARRAY:
+ if (lhs.m_val.data.arr_float.size != rhs.m_val.data.arr_float.size) {
+ return false;
+ }
+ return std::memcmp(lhs.m_val.data.arr_float.arr,
+ rhs.m_val.data.arr_float.arr,
+ lhs.m_val.data.arr_float.size *
+ sizeof(lhs.m_val.data.arr_float.arr[0])) == 0;
case NT_DOUBLE_ARRAY:
if (lhs.m_val.data.arr_double.size != rhs.m_val.data.arr_double.size) {
return false;
@@ -228,7 +345,8 @@
lhs.m_val.data.arr_double.size *
sizeof(lhs.m_val.data.arr_double.arr[0])) == 0;
case NT_STRING_ARRAY:
- return lhs.m_string_array == rhs.m_string_array;
+ return static_cast<StringArrayStorage*>(lhs.m_storage.get())->strings ==
+ static_cast<StringArrayStorage*>(rhs.m_storage.get())->strings;
default:
// assert(false && "unknown value type");
return false;
diff --git a/ntcore/src/main/native/cpp/Value_internal.cpp b/ntcore/src/main/native/cpp/Value_internal.cpp
new file mode 100644
index 0000000..2003d31
--- /dev/null
+++ b/ntcore/src/main/native/cpp/Value_internal.cpp
@@ -0,0 +1,49 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "Value_internal.h"
+
+using namespace nt;
+
+Value nt::ConvertNumericValue(const Value& value, NT_Type type) {
+ switch (type) {
+ case NT_INTEGER: {
+ Value newval =
+ Value::MakeInteger(GetNumericAs<int64_t>(value), value.time());
+ newval.SetServerTime(value.server_time());
+ return newval;
+ }
+ case NT_FLOAT: {
+ Value newval = Value::MakeFloat(GetNumericAs<float>(value), value.time());
+ newval.SetServerTime(value.server_time());
+ return newval;
+ }
+ case NT_DOUBLE: {
+ Value newval =
+ Value::MakeDouble(GetNumericAs<double>(value), value.time());
+ newval.SetServerTime(value.server_time());
+ return newval;
+ }
+ case NT_INTEGER_ARRAY: {
+ Value newval = Value::MakeIntegerArray(GetNumericArrayAs<int64_t>(value),
+ value.time());
+ newval.SetServerTime(value.server_time());
+ return newval;
+ }
+ case NT_FLOAT_ARRAY: {
+ Value newval =
+ Value::MakeFloatArray(GetNumericArrayAs<float>(value), value.time());
+ newval.SetServerTime(value.server_time());
+ return newval;
+ }
+ case NT_DOUBLE_ARRAY: {
+ Value newval = Value::MakeDoubleArray(GetNumericArrayAs<double>(value),
+ value.time());
+ newval.SetServerTime(value.server_time());
+ return newval;
+ }
+ default:
+ return {};
+ }
+}
diff --git a/ntcore/src/main/native/cpp/Value_internal.h b/ntcore/src/main/native/cpp/Value_internal.h
index 54850ab..03532ac 100644
--- a/ntcore/src/main/native/cpp/Value_internal.h
+++ b/ntcore/src/main/native/cpp/Value_internal.h
@@ -2,26 +2,90 @@
// 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.
-#ifndef NTCORE_VALUE_INTERNAL_H_
-#define NTCORE_VALUE_INTERNAL_H_
+#pragma once
+#include <cstring>
#include <memory>
#include <string>
#include <string_view>
+#include <vector>
+#include <wpi/MemAlloc.h>
+
+#include "networktables/NetworkTableValue.h"
#include "ntcore_c.h"
namespace nt {
class Value;
+template <typename T>
+inline void ConvertToC(const T& in, T* out) {
+ *out = in;
+}
+
void ConvertToC(const Value& in, NT_Value* out);
-std::shared_ptr<Value> ConvertFromC(const NT_Value& value);
+Value ConvertFromC(const NT_Value& value);
+size_t ConvertToC(std::string_view in, char** out);
void ConvertToC(std::string_view in, NT_String* out);
inline std::string_view ConvertFromC(const NT_String& str) {
return {str.str, str.len};
}
-} // namespace nt
+template <typename O, typename I>
+O* ConvertToC(const std::vector<I>& in, size_t* out_len) {
+ if (!out_len) {
+ return nullptr;
+ }
+ *out_len = in.size();
+ if (in.empty()) {
+ return nullptr;
+ }
+ O* out = static_cast<O*>(wpi::safe_malloc(sizeof(O) * in.size()));
+ for (size_t i = 0; i < in.size(); ++i) {
+ ConvertToC(in[i], &out[i]);
+ }
+ return out;
+}
-#endif // NTCORE_VALUE_INTERNAL_H_
+template <typename O, typename I>
+O* ConvertToC(const std::basic_string<I>& in, size_t* out_len) {
+ char* out = static_cast<char*>(wpi::safe_malloc(in.size() + 1));
+ std::memmove(out, in.data(), in.size()); // NOLINT
+ out[in.size()] = '\0';
+ *out_len = in.size();
+ return out;
+}
+
+template <typename T>
+T GetNumericAs(const Value& value) {
+ if (value.IsInteger()) {
+ return static_cast<T>(value.GetInteger());
+ } else if (value.IsFloat()) {
+ return static_cast<T>(value.GetFloat());
+ } else if (value.IsDouble()) {
+ return static_cast<T>(value.GetDouble());
+ } else {
+ return {};
+ }
+}
+
+template <typename T>
+std::vector<T> GetNumericArrayAs(const Value& value) {
+ if (value.IsIntegerArray()) {
+ auto arr = value.GetIntegerArray();
+ return {arr.begin(), arr.end()};
+ } else if (value.IsFloatArray()) {
+ auto arr = value.GetFloatArray();
+ return {arr.begin(), arr.end()};
+ } else if (value.IsDoubleArray()) {
+ auto arr = value.GetDoubleArray();
+ return {arr.begin(), arr.end()};
+ } else {
+ return {};
+ }
+}
+
+Value ConvertNumericValue(const Value& value, NT_Type type);
+
+} // namespace nt
diff --git a/ntcore/src/main/native/cpp/WireDecoder.cpp b/ntcore/src/main/native/cpp/WireDecoder.cpp
deleted file mode 100644
index c0647dc..0000000
--- a/ntcore/src/main/native/cpp/WireDecoder.cpp
+++ /dev/null
@@ -1,247 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "WireDecoder.h"
-
-#include <stdint.h>
-
-#include <cassert>
-#include <cstdlib>
-#include <cstring>
-
-#include <wpi/MathExtras.h>
-#include <wpi/MemAlloc.h>
-#include <wpi/leb128.h>
-
-using namespace nt;
-
-static double ReadDouble(const char*& buf) {
- // Fast but non-portable!
- uint64_t val = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- val <<= 8;
- val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- val <<= 8;
- val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- val <<= 8;
- val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- val <<= 8;
- val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- val <<= 8;
- val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- val <<= 8;
- val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- val <<= 8;
- val |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- return wpi::BitsToDouble(val);
-}
-
-WireDecoder::WireDecoder(wpi::raw_istream& is, unsigned int proto_rev,
- wpi::Logger& logger)
- : m_is(is), m_logger(logger) {
- // Start with a 1K temporary buffer. Use malloc instead of new so we can
- // realloc.
- m_allocated = 1024;
- m_buf = static_cast<char*>(wpi::safe_malloc(m_allocated));
- m_proto_rev = proto_rev;
- m_error = nullptr;
-}
-
-WireDecoder::~WireDecoder() {
- std::free(m_buf);
-}
-
-bool WireDecoder::ReadDouble(double* val) {
- const char* buf;
- if (!Read(&buf, 8)) {
- return false;
- }
- *val = ::ReadDouble(buf);
- return true;
-}
-
-void WireDecoder::Realloc(size_t len) {
- // Double current buffer size until we have enough space.
- if (m_allocated >= len) {
- return;
- }
- size_t newlen = m_allocated * 2;
- while (newlen < len) {
- newlen *= 2;
- }
- m_buf = static_cast<char*>(wpi::safe_realloc(m_buf, newlen));
- m_allocated = newlen;
-}
-
-bool WireDecoder::ReadType(NT_Type* type) {
- unsigned int itype;
- if (!Read8(&itype)) {
- return false;
- }
- // Convert from byte value to enum
- switch (itype) {
- case 0x00:
- *type = NT_BOOLEAN;
- break;
- case 0x01:
- *type = NT_DOUBLE;
- break;
- case 0x02:
- *type = NT_STRING;
- break;
- case 0x03:
- *type = NT_RAW;
- break;
- case 0x10:
- *type = NT_BOOLEAN_ARRAY;
- break;
- case 0x11:
- *type = NT_DOUBLE_ARRAY;
- break;
- case 0x12:
- *type = NT_STRING_ARRAY;
- break;
- case 0x20:
- *type = NT_RPC;
- break;
- default:
- *type = NT_UNASSIGNED;
- m_error = "unrecognized value type";
- return false;
- }
- return true;
-}
-
-std::shared_ptr<Value> WireDecoder::ReadValue(NT_Type type) {
- switch (type) {
- case NT_BOOLEAN: {
- unsigned int v;
- if (!Read8(&v)) {
- return nullptr;
- }
- return Value::MakeBoolean(v != 0);
- }
- case NT_DOUBLE: {
- double v;
- if (!ReadDouble(&v)) {
- return nullptr;
- }
- return Value::MakeDouble(v);
- }
- case NT_STRING: {
- std::string v;
- if (!ReadString(&v)) {
- return nullptr;
- }
- return Value::MakeString(std::move(v));
- }
- case NT_RAW: {
- if (m_proto_rev < 0x0300u) {
- m_error = "received raw value in protocol < 3.0";
- return nullptr;
- }
- std::string v;
- if (!ReadString(&v)) {
- return nullptr;
- }
- return Value::MakeRaw(std::move(v));
- }
- case NT_RPC: {
- if (m_proto_rev < 0x0300u) {
- m_error = "received RPC value in protocol < 3.0";
- return nullptr;
- }
- std::string v;
- if (!ReadString(&v)) {
- return nullptr;
- }
- return Value::MakeRpc(std::move(v));
- }
- case NT_BOOLEAN_ARRAY: {
- // size
- unsigned int size;
- if (!Read8(&size)) {
- return nullptr;
- }
-
- // array values
- const char* buf;
- if (!Read(&buf, size)) {
- return nullptr;
- }
- std::vector<int> v(size);
- for (unsigned int i = 0; i < size; ++i) {
- v[i] = buf[i] ? 1 : 0;
- }
- return Value::MakeBooleanArray(std::move(v));
- }
- case NT_DOUBLE_ARRAY: {
- // size
- unsigned int size;
- if (!Read8(&size)) {
- return nullptr;
- }
-
- // array values
- const char* buf;
- if (!Read(&buf, size * 8)) {
- return nullptr;
- }
- std::vector<double> v(size);
- for (unsigned int i = 0; i < size; ++i) {
- v[i] = ::ReadDouble(buf);
- }
- return Value::MakeDoubleArray(std::move(v));
- }
- case NT_STRING_ARRAY: {
- // size
- unsigned int size;
- if (!Read8(&size)) {
- return nullptr;
- }
-
- // array values
- std::vector<std::string> v(size);
- for (unsigned int i = 0; i < size; ++i) {
- if (!ReadString(&v[i])) {
- return nullptr;
- }
- }
- return Value::MakeStringArray(std::move(v));
- }
- default:
- m_error = "invalid type when trying to read value";
- return nullptr;
- }
-}
-
-bool WireDecoder::ReadString(std::string* str) {
- size_t len;
- if (m_proto_rev < 0x0300u) {
- unsigned int v;
- if (!Read16(&v)) {
- return false;
- }
- len = v;
- } else {
- uint64_t v;
- if (!ReadUleb128(&v)) {
- return false;
- }
- len = v;
- }
- const char* buf;
- if (!Read(&buf, len)) {
- return false;
- }
- str->assign(buf, len);
- return true;
-}
diff --git a/ntcore/src/main/native/cpp/WireDecoder.h b/ntcore/src/main/native/cpp/WireDecoder.h
deleted file mode 100644
index 972be57..0000000
--- a/ntcore/src/main/native/cpp/WireDecoder.h
+++ /dev/null
@@ -1,163 +0,0 @@
-// 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.
-
-#ifndef NTCORE_WIREDECODER_H_
-#define NTCORE_WIREDECODER_H_
-
-#include <stdint.h>
-
-#include <cstddef>
-#include <memory>
-#include <string>
-
-#include <wpi/leb128.h>
-#include <wpi/raw_istream.h>
-
-#include "Log.h"
-#include "networktables/NetworkTableValue.h"
-
-namespace nt {
-
-/* Decodes network data into native representation.
- * This class is designed to read from a raw_istream, which provides a blocking
- * read interface. There are no provisions in this class for resuming a read
- * that was interrupted partway. Read functions return false if
- * raw_istream.read() returned false (indicating the end of the input data
- * stream).
- */
-class WireDecoder {
- public:
- WireDecoder(wpi::raw_istream& is, unsigned int proto_rev,
- wpi::Logger& logger);
- ~WireDecoder();
-
- void set_proto_rev(unsigned int proto_rev) { m_proto_rev = proto_rev; }
-
- /* Get the active protocol revision. */
- unsigned int proto_rev() const { return m_proto_rev; }
-
- /* Get the logger. */
- wpi::Logger& logger() const { return m_logger; }
-
- /* Clears error indicator. */
- void Reset() { m_error = nullptr; }
-
- /* Returns error indicator (a string describing the error). Returns nullptr
- * if no error has occurred.
- */
- const char* error() const { return m_error; }
-
- void set_error(const char* error) { m_error = error; }
-
- /* Reads the specified number of bytes.
- * @param buf pointer to read data (output parameter)
- * @param len number of bytes to read
- * Caution: the buffer is only temporarily valid.
- */
- bool Read(const char** buf, size_t len) {
- if (len > m_allocated) {
- Realloc(len);
- }
- *buf = m_buf;
- m_is.read(m_buf, len);
-#if 0
- if (m_logger.min_level() <= NT_LOG_DEBUG4 && m_logger.HasLogger()) {
- std::ostringstream oss;
- oss << "read " << len << " bytes:" << std::hex;
- if (!rv) {
- oss << "error";
- } else {
- for (size_t i = 0; i < len; ++i)
- oss << ' ' << static_cast<unsigned int>((*buf)[i]);
- }
- m_logger.Log(NT_LOG_DEBUG4, __FILE__, __LINE__, oss.str().c_str());
- }
-#endif
- return !m_is.has_error();
- }
-
- /* Reads a single byte. */
- bool Read8(unsigned int* val) {
- const char* buf;
- if (!Read(&buf, 1)) {
- return false;
- }
- *val = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- return true;
- }
-
- /* Reads a 16-bit word. */
- bool Read16(unsigned int* val) {
- const char* buf;
- if (!Read(&buf, 2)) {
- return false;
- }
- unsigned int v = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- v <<= 8;
- v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- *val = v;
- return true;
- }
-
- /* Reads a 32-bit word. */
- bool Read32(uint32_t* val) {
- const char* buf;
- if (!Read(&buf, 4)) {
- return false;
- }
- unsigned int v = (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- v <<= 8;
- v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- v <<= 8;
- v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- ++buf;
- v <<= 8;
- v |= (*reinterpret_cast<const unsigned char*>(buf)) & 0xff;
- *val = v;
- return true;
- }
-
- /* Reads a double. */
- bool ReadDouble(double* val);
-
- /* Reads an ULEB128-encoded unsigned integer. */
- bool ReadUleb128(uint64_t* val) { return wpi::ReadUleb128(m_is, val); }
-
- bool ReadType(NT_Type* type);
- bool ReadString(std::string* str);
- std::shared_ptr<Value> ReadValue(NT_Type type);
-
- WireDecoder(const WireDecoder&) = delete;
- WireDecoder& operator=(const WireDecoder&) = delete;
-
- protected:
- /* The protocol revision. E.g. 0x0200 for version 2.0. */
- unsigned int m_proto_rev;
-
- /* Error indicator. */
- const char* m_error;
-
- private:
- /* Reallocate temporary buffer to specified length. */
- void Realloc(size_t len);
-
- /* input stream */
- wpi::raw_istream& m_is;
-
- /* logger */
- wpi::Logger& m_logger;
-
- /* temporary buffer */
- char* m_buf;
-
- /* allocated size of temporary buffer */
- size_t m_allocated;
-};
-
-} // namespace nt
-
-#endif // NTCORE_WIREDECODER_H_
diff --git a/ntcore/src/main/native/cpp/WireEncoder.cpp b/ntcore/src/main/native/cpp/WireEncoder.cpp
deleted file mode 100644
index b35b780..0000000
--- a/ntcore/src/main/native/cpp/WireEncoder.cpp
+++ /dev/null
@@ -1,226 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "WireEncoder.h"
-
-#include <stdint.h>
-
-#include <cassert>
-#include <cstdlib>
-#include <cstring>
-
-#include <wpi/MathExtras.h>
-#include <wpi/leb128.h>
-
-using namespace nt;
-
-WireEncoder::WireEncoder(unsigned int proto_rev) {
- m_proto_rev = proto_rev;
- m_error = nullptr;
-}
-
-void WireEncoder::WriteDouble(double val) {
- // The highest performance way to do this, albeit non-portable.
- uint64_t v = wpi::DoubleToBits(val);
- m_data.append(
- {static_cast<char>((v >> 56) & 0xff), static_cast<char>((v >> 48) & 0xff),
- static_cast<char>((v >> 40) & 0xff), static_cast<char>((v >> 32) & 0xff),
- static_cast<char>((v >> 24) & 0xff), static_cast<char>((v >> 16) & 0xff),
- static_cast<char>((v >> 8) & 0xff), static_cast<char>(v & 0xff)});
-}
-
-void WireEncoder::WriteUleb128(uint32_t val) {
- wpi::WriteUleb128(m_data, val);
-}
-
-void WireEncoder::WriteType(NT_Type type) {
- char ch;
- // Convert from enum to actual byte value.
- switch (type) {
- case NT_BOOLEAN:
- ch = 0x00;
- break;
- case NT_DOUBLE:
- ch = 0x01;
- break;
- case NT_STRING:
- ch = 0x02;
- break;
- case NT_RAW:
- if (m_proto_rev < 0x0300u) {
- m_error = "raw type not supported in protocol < 3.0";
- return;
- }
- ch = 0x03;
- break;
- case NT_BOOLEAN_ARRAY:
- ch = 0x10;
- break;
- case NT_DOUBLE_ARRAY:
- ch = 0x11;
- break;
- case NT_STRING_ARRAY:
- ch = 0x12;
- break;
- case NT_RPC:
- if (m_proto_rev < 0x0300u) {
- m_error = "RPC type not supported in protocol < 3.0";
- return;
- }
- ch = 0x20;
- break;
- default:
- m_error = "unrecognized type";
- return;
- }
- m_data.push_back(ch);
-}
-
-size_t WireEncoder::GetValueSize(const Value& value) const {
- switch (value.type()) {
- case NT_BOOLEAN:
- return 1;
- case NT_DOUBLE:
- return 8;
- case NT_STRING:
- return GetStringSize(value.GetString());
- case NT_RAW:
- if (m_proto_rev < 0x0300u) {
- return 0;
- }
- return GetStringSize(value.GetRaw());
- case NT_RPC:
- if (m_proto_rev < 0x0300u) {
- return 0;
- }
- return GetStringSize(value.GetRpc());
- case NT_BOOLEAN_ARRAY: {
- // 1-byte size, 1 byte per element
- size_t size = value.GetBooleanArray().size();
- if (size > 0xff) {
- size = 0xff; // size is only 1 byte, truncate
- }
- return 1 + size;
- }
- case NT_DOUBLE_ARRAY: {
- // 1-byte size, 8 bytes per element
- size_t size = value.GetDoubleArray().size();
- if (size > 0xff) {
- size = 0xff; // size is only 1 byte, truncate
- }
- return 1 + size * 8;
- }
- case NT_STRING_ARRAY: {
- auto v = value.GetStringArray();
- size_t size = v.size();
- if (size > 0xff) {
- size = 0xff; // size is only 1 byte, truncate
- }
- size_t len = 1; // 1-byte size
- for (size_t i = 0; i < size; ++i) {
- len += GetStringSize(v[i]);
- }
- return len;
- }
- default:
- return 0;
- }
-}
-
-void WireEncoder::WriteValue(const Value& value) {
- switch (value.type()) {
- case NT_BOOLEAN:
- Write8(value.GetBoolean() ? 1 : 0);
- break;
- case NT_DOUBLE:
- WriteDouble(value.GetDouble());
- break;
- case NT_STRING:
- WriteString(value.GetString());
- break;
- case NT_RAW:
- if (m_proto_rev < 0x0300u) {
- m_error = "raw values not supported in protocol < 3.0";
- return;
- }
- WriteString(value.GetRaw());
- break;
- case NT_RPC:
- if (m_proto_rev < 0x0300u) {
- m_error = "RPC values not supported in protocol < 3.0";
- return;
- }
- WriteString(value.GetRpc());
- break;
- case NT_BOOLEAN_ARRAY: {
- auto v = value.GetBooleanArray();
- size_t size = v.size();
- if (size > 0xff) {
- size = 0xff; // size is only 1 byte, truncate
- }
- Write8(size);
-
- for (size_t i = 0; i < size; ++i) {
- Write8(v[i] ? 1 : 0);
- }
- break;
- }
- case NT_DOUBLE_ARRAY: {
- auto v = value.GetDoubleArray();
- size_t size = v.size();
- if (size > 0xff) {
- size = 0xff; // size is only 1 byte, truncate
- }
- Write8(size);
-
- for (size_t i = 0; i < size; ++i) {
- WriteDouble(v[i]);
- }
- break;
- }
- case NT_STRING_ARRAY: {
- auto v = value.GetStringArray();
- size_t size = v.size();
- if (size > 0xff) {
- size = 0xff; // size is only 1 byte, truncate
- }
- Write8(size);
-
- for (size_t i = 0; i < size; ++i) {
- WriteString(v[i]);
- }
- break;
- }
- default:
- m_error = "unrecognized type when writing value";
- return;
- }
-}
-
-size_t WireEncoder::GetStringSize(std::string_view str) const {
- if (m_proto_rev < 0x0300u) {
- size_t len = str.size();
- if (len > 0xffff) {
- len = 0xffff; // Limited to 64K length; truncate
- }
- return 2 + len;
- }
- return wpi::SizeUleb128(str.size()) + str.size();
-}
-
-void WireEncoder::WriteString(std::string_view str) {
- // length
- size_t len = str.size();
- if (m_proto_rev < 0x0300u) {
- if (len > 0xffff) {
- len = 0xffff; // Limited to 64K length; truncate
- }
- Write16(len);
- } else {
- WriteUleb128(len);
- }
-
- // contents
- m_data.append(str.data(), str.data() + len);
-}
diff --git a/ntcore/src/main/native/cpp/WireEncoder.h b/ntcore/src/main/native/cpp/WireEncoder.h
deleted file mode 100644
index 84edc39..0000000
--- a/ntcore/src/main/native/cpp/WireEncoder.h
+++ /dev/null
@@ -1,108 +0,0 @@
-// 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.
-
-#ifndef NTCORE_WIREENCODER_H_
-#define NTCORE_WIREENCODER_H_
-
-#include <stdint.h>
-
-#include <cassert>
-#include <cstddef>
-#include <string_view>
-
-#include <wpi/SmallVector.h>
-
-#include "networktables/NetworkTableValue.h"
-
-namespace nt {
-
-/* Encodes native data for network transmission.
- * This class maintains an internal memory buffer for written data so that
- * it can be efficiently bursted to the network after a number of writes
- * have been performed. For this reason, all operations are non-blocking.
- */
-class WireEncoder {
- public:
- explicit WireEncoder(unsigned int proto_rev);
-
- /* Change the protocol revision (mostly affects value encoding). */
- void set_proto_rev(unsigned int proto_rev) { m_proto_rev = proto_rev; }
-
- /* Get the active protocol revision. */
- unsigned int proto_rev() const { return m_proto_rev; }
-
- /* Clears buffer and error indicator. */
- void Reset() {
- m_data.clear();
- m_error = nullptr;
- }
-
- /* Returns error indicator (a string describing the error). Returns nullptr
- * if no error has occurred.
- */
- const char* error() const { return m_error; }
-
- /* Returns pointer to start of memory buffer with written data. */
- const char* data() const { return m_data.data(); }
-
- /* Returns number of bytes written to memory buffer. */
- size_t size() const { return m_data.size(); }
-
- std::string_view ToStringView() const {
- return {m_data.data(), m_data.size()};
- }
-
- /* Writes a single byte. */
- void Write8(unsigned int val) {
- m_data.push_back(static_cast<char>(val & 0xff));
- }
-
- /* Writes a 16-bit word. */
- void Write16(unsigned int val) {
- m_data.append(
- {static_cast<char>((val >> 8) & 0xff), static_cast<char>(val & 0xff)});
- }
-
- /* Writes a 32-bit word. */
- void Write32(uint32_t val) {
- m_data.append({static_cast<char>((val >> 24) & 0xff),
- static_cast<char>((val >> 16) & 0xff),
- static_cast<char>((val >> 8) & 0xff),
- static_cast<char>(val & 0xff)});
- }
-
- /* Writes a double. */
- void WriteDouble(double val);
-
- /* Writes an ULEB128-encoded unsigned integer. */
- void WriteUleb128(uint32_t val);
-
- void WriteType(NT_Type type);
- void WriteValue(const Value& value);
- void WriteString(std::string_view str);
-
- /* Utility function to get the written size of a value (without actually
- * writing it).
- */
- size_t GetValueSize(const Value& value) const;
-
- /* Utility function to get the written size of a string (without actually
- * writing it).
- */
- size_t GetStringSize(std::string_view str) const;
-
- protected:
- /* The protocol revision. E.g. 0x0200 for version 2.0. */
- unsigned int m_proto_rev;
-
- /* Error indicator. */
- const char* m_error;
-
- private:
- wpi::SmallVector<char, 256> m_data;
-};
-
-} // namespace nt
-
-#endif // NTCORE_WIREENCODER_H_
diff --git a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
index 6c7ed82..b868604 100644
--- a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
+++ b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp
@@ -9,9 +9,11 @@
#include <fmt/format.h>
#include <wpi/ConvertUTF.h>
#include <wpi/jni_util.h>
+#include <wpi/json.h>
#include "edu_wpi_first_networktables_NetworkTablesJNI.h"
#include "ntcore.h"
+#include "ntcore_cpp.h"
using namespace wpi::java;
@@ -19,6 +21,11 @@
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
+namespace nt {
+bool JNI_LoadTypes(JNIEnv* env);
+void JNI_UnloadTypes(JNIEnv* env);
+} // namespace nt
+
//
// Globals and load/unload
//
@@ -27,35 +34,40 @@
static JavaVM* jvm = nullptr;
static JClass booleanCls;
static JClass connectionInfoCls;
-static JClass connectionNotificationCls;
static JClass doubleCls;
-static JClass entryInfoCls;
-static JClass entryNotificationCls;
+static JClass eventCls;
+static JClass floatCls;
static JClass logMessageCls;
-static JClass rpcAnswerCls;
+static JClass longCls;
+static JClass optionalLongCls;
+static JClass pubSubOptionsCls;
+static JClass timeSyncEventDataCls;
+static JClass topicInfoCls;
static JClass valueCls;
+static JClass valueEventDataCls;
static JException illegalArgEx;
static JException interruptedEx;
static JException nullPointerEx;
-static JException persistentEx;
static const JClassInit classes[] = {
{"java/lang/Boolean", &booleanCls},
{"edu/wpi/first/networktables/ConnectionInfo", &connectionInfoCls},
- {"edu/wpi/first/networktables/ConnectionNotification",
- &connectionNotificationCls},
{"java/lang/Double", &doubleCls},
- {"edu/wpi/first/networktables/EntryInfo", &entryInfoCls},
- {"edu/wpi/first/networktables/EntryNotification", &entryNotificationCls},
+ {"edu/wpi/first/networktables/NetworkTableEvent", &eventCls},
+ {"java/lang/Float", &floatCls},
{"edu/wpi/first/networktables/LogMessage", &logMessageCls},
- {"edu/wpi/first/networktables/RpcAnswer", &rpcAnswerCls},
- {"edu/wpi/first/networktables/NetworkTableValue", &valueCls}};
+ {"java/lang/Long", &longCls},
+ {"java/util/OptionalLong", &optionalLongCls},
+ {"edu/wpi/first/networktables/PubSubOptions", &pubSubOptionsCls},
+ {"edu/wpi/first/networktables/TimeSyncEventData", &timeSyncEventDataCls},
+ {"edu/wpi/first/networktables/TopicInfo", &topicInfoCls},
+ {"edu/wpi/first/networktables/NetworkTableValue", &valueCls},
+ {"edu/wpi/first/networktables/ValueEventData", &valueEventDataCls}};
static const JExceptionInit exceptions[] = {
{"java/lang/IllegalArgumentException", &illegalArgEx},
{"java/lang/InterruptedException", &interruptedEx},
- {"java/lang/NullPointerException", &nullPointerEx},
- {"edu/wpi/first/networktables/PersistentException", &persistentEx}};
+ {"java/lang/NullPointerException", &nullPointerEx}};
extern "C" {
@@ -82,6 +94,10 @@
}
}
+ if (!nt::JNI_LoadTypes(env)) {
+ return JNI_ERR;
+ }
+
return JNI_VERSION_1_6;
}
@@ -97,6 +113,7 @@
for (auto& c : exceptions) {
c.cls->free(env);
}
+ nt::JNI_UnloadTypes(env);
jvm = nullptr;
}
@@ -106,92 +123,98 @@
// Conversions from Java objects to C++
//
-inline std::shared_ptr<nt::Value> FromJavaRaw(JNIEnv* env, jbyteArray jarr,
- jlong time) {
- CriticalJByteArrayRef ref{env, jarr};
- if (!ref) {
- return nullptr;
+static nt::PubSubOptions FromJavaPubSubOptions(JNIEnv* env, jobject joptions) {
+ if (!joptions) {
+ return {};
}
- return nt::Value::MakeRaw(ref.str(), time);
-}
+#define FIELD(name, sig) \
+ static jfieldID name##Field = nullptr; \
+ if (!name##Field) { \
+ name##Field = env->GetFieldID(pubSubOptionsCls, #name, sig); \
+ }
-inline std::shared_ptr<nt::Value> FromJavaRawBB(JNIEnv* env, jobject jbb,
- int len, jlong time) {
- JByteArrayRef ref{env, jbb, len};
- if (!ref) {
- return nullptr;
- }
- return nt::Value::MakeRaw(ref.str(), time);
-}
+ FIELD(pollStorage, "I");
+ FIELD(periodic, "D");
+ FIELD(excludePublisher, "I");
+ FIELD(sendAll, "Z");
+ FIELD(topicsOnly, "Z");
+ FIELD(keepDuplicates, "Z");
+ FIELD(prefixMatch, "Z");
+ FIELD(disableRemote, "Z");
+ FIELD(disableLocal, "Z");
+ FIELD(excludeSelf, "Z");
-inline std::shared_ptr<nt::Value> FromJavaRpc(JNIEnv* env, jbyteArray jarr,
- jlong time) {
- CriticalJByteArrayRef ref{env, jarr};
- if (!ref) {
- return nullptr;
- }
- return nt::Value::MakeRpc(ref.str(), time);
-}
+#undef FIELD
-std::shared_ptr<nt::Value> FromJavaBooleanArray(JNIEnv* env, jbooleanArray jarr,
- jlong time) {
- CriticalJBooleanArrayRef ref{env, jarr};
- if (!ref) {
- return nullptr;
- }
- wpi::span<const jboolean> elements{ref};
- size_t len = elements.size();
- std::vector<int> arr;
- arr.reserve(len);
- for (size_t i = 0; i < len; ++i) {
- arr.push_back(elements[i]);
- }
- return nt::Value::MakeBooleanArray(arr, time);
-}
+#define FIELD(ctype, jtype, name) \
+ .name = static_cast<ctype>(env->Get##jtype##Field(joptions, name##Field))
-std::shared_ptr<nt::Value> FromJavaDoubleArray(JNIEnv* env, jdoubleArray jarr,
- jlong time) {
- CriticalJDoubleArrayRef ref{env, jarr};
- if (!ref) {
- return nullptr;
- }
- return nt::Value::MakeDoubleArray(ref, time);
-}
+ return {FIELD(unsigned int, Int, pollStorage),
+ FIELD(double, Double, periodic),
+ FIELD(NT_Publisher, Int, excludePublisher),
+ FIELD(bool, Boolean, sendAll),
+ FIELD(bool, Boolean, topicsOnly),
+ FIELD(bool, Boolean, keepDuplicates),
+ FIELD(bool, Boolean, prefixMatch),
+ FIELD(bool, Boolean, disableRemote),
+ FIELD(bool, Boolean, disableLocal),
+ FIELD(bool, Boolean, excludeSelf)};
-std::shared_ptr<nt::Value> FromJavaStringArray(JNIEnv* env, jobjectArray jarr,
- jlong time) {
- size_t len = env->GetArrayLength(jarr);
- std::vector<std::string> arr;
- arr.reserve(len);
- for (size_t i = 0; i < len; ++i) {
- JLocal<jstring> elem{
- env, static_cast<jstring>(env->GetObjectArrayElement(jarr, i))};
- if (!elem) {
- return nullptr;
- }
- arr.emplace_back(JStringRef{env, elem}.str());
- }
- return nt::Value::MakeStringArray(std::move(arr), time);
+#undef GET
+#undef FIELD
}
//
// Conversions from C++ to Java objects
//
+static jobject MakeJObject(JNIEnv* env, std::optional<int64_t> value) {
+ static jmethodID emptyMethod = nullptr;
+ static jmethodID ofMethod = nullptr;
+ if (!emptyMethod) {
+ emptyMethod = env->GetStaticMethodID(optionalLongCls, "empty",
+ "()Ljava/util/OptionalLong;");
+ }
+ if (!ofMethod) {
+ ofMethod = env->GetStaticMethodID(optionalLongCls, "of",
+ "(J)Ljava/util/OptionalLong;");
+ }
+ if (value) {
+ return env->CallStaticObjectMethod(optionalLongCls, ofMethod,
+ static_cast<jlong>(*value));
+ } else {
+ return env->CallStaticObjectMethod(optionalLongCls, emptyMethod);
+ }
+}
+
static jobject MakeJObject(JNIEnv* env, const nt::Value& value) {
static jmethodID booleanConstructor = nullptr;
static jmethodID doubleConstructor = nullptr;
+ static jmethodID floatConstructor = nullptr;
+ static jmethodID longConstructor = nullptr;
if (!booleanConstructor) {
booleanConstructor = env->GetMethodID(booleanCls, "<init>", "(Z)V");
}
if (!doubleConstructor) {
doubleConstructor = env->GetMethodID(doubleCls, "<init>", "(D)V");
}
+ if (!floatConstructor) {
+ floatConstructor = env->GetMethodID(floatCls, "<init>", "(F)V");
+ }
+ if (!longConstructor) {
+ longConstructor = env->GetMethodID(longCls, "<init>", "(J)V");
+ }
switch (value.type()) {
case NT_BOOLEAN:
return env->NewObject(booleanCls, booleanConstructor,
static_cast<jboolean>(value.GetBoolean() ? 1 : 0));
+ case NT_INTEGER:
+ return env->NewObject(longCls, longConstructor,
+ static_cast<jlong>(value.GetInteger()));
+ case NT_FLOAT:
+ return env->NewObject(floatCls, floatConstructor,
+ static_cast<jfloat>(value.GetFloat()));
case NT_DOUBLE:
return env->NewObject(doubleCls, doubleConstructor,
static_cast<jdouble>(value.GetDouble()));
@@ -201,28 +224,31 @@
return MakeJByteArray(env, value.GetRaw());
case NT_BOOLEAN_ARRAY:
return MakeJBooleanArray(env, value.GetBooleanArray());
+ case NT_INTEGER_ARRAY:
+ return MakeJLongArray(env, value.GetIntegerArray());
+ case NT_FLOAT_ARRAY:
+ return MakeJFloatArray(env, value.GetFloatArray());
case NT_DOUBLE_ARRAY:
return MakeJDoubleArray(env, value.GetDoubleArray());
case NT_STRING_ARRAY:
return MakeJStringArray(env, value.GetStringArray());
- case NT_RPC:
- return MakeJByteArray(env, value.GetRpc());
default:
return nullptr;
}
}
-static jobject MakeJValue(JNIEnv* env, const nt::Value* value) {
+static jobject MakeJValue(JNIEnv* env, const nt::Value& value) {
static jmethodID constructor =
- env->GetMethodID(valueCls, "<init>", "(ILjava/lang/Object;J)V");
+ env->GetMethodID(valueCls, "<init>", "(ILjava/lang/Object;JJ)V");
if (!value) {
return env->NewObject(valueCls, constructor,
static_cast<jint>(NT_UNASSIGNED), nullptr,
- static_cast<jlong>(0));
+ static_cast<jlong>(0), static_cast<jlong>(0));
}
- return env->NewObject(valueCls, constructor, static_cast<jint>(value->type()),
- MakeJObject(env, *value),
- static_cast<jlong>(value->time()));
+ return env->NewObject(valueCls, constructor, static_cast<jint>(value.type()),
+ MakeJObject(env, value),
+ static_cast<jlong>(value.time()),
+ static_cast<jlong>(value.server_time()));
}
static jobject MakeJObject(JNIEnv* env, const nt::ConnectionInfo& info) {
@@ -237,120 +263,96 @@
static_cast<jint>(info.protocol_version));
}
-static jobject MakeJObject(JNIEnv* env, jobject inst,
- const nt::ConnectionNotification& notification) {
+static jobject MakeJObject(JNIEnv* env, const nt::LogMessage& msg) {
static jmethodID constructor = env->GetMethodID(
- connectionNotificationCls, "<init>",
- "(Ledu/wpi/first/networktables/NetworkTableInstance;IZLedu/wpi/first/"
- "networktables/ConnectionInfo;)V");
- JLocal<jobject> conn{env, MakeJObject(env, notification.conn)};
- return env->NewObject(connectionNotificationCls, constructor, inst,
- static_cast<jint>(notification.listener),
- static_cast<jboolean>(notification.connected),
- conn.obj());
-}
-
-static jobject MakeJObject(JNIEnv* env, jobject inst,
- const nt::EntryInfo& info) {
- static jmethodID constructor =
- env->GetMethodID(entryInfoCls, "<init>",
- "(Ledu/wpi/first/networktables/"
- "NetworkTableInstance;ILjava/lang/String;IIJ)V");
- JLocal<jstring> name{env, MakeJString(env, info.name)};
- return env->NewObject(
- entryInfoCls, constructor, inst, static_cast<jint>(info.entry),
- name.obj(), static_cast<jint>(info.type), static_cast<jint>(info.flags),
- static_cast<jlong>(info.last_change));
-}
-
-static jobject MakeJObject(JNIEnv* env, jobject inst,
- const nt::EntryNotification& notification) {
- static jmethodID constructor = env->GetMethodID(
- entryNotificationCls, "<init>",
- "(Ledu/wpi/first/networktables/NetworkTableInstance;IILjava/lang/"
- "String;Ledu/wpi/first/networktables/NetworkTableValue;I)V");
- JLocal<jstring> name{env, MakeJString(env, notification.name)};
- JLocal<jobject> value{env, MakeJValue(env, notification.value.get())};
- return env->NewObject(entryNotificationCls, constructor, inst,
- static_cast<jint>(notification.listener),
- static_cast<jint>(notification.entry), name.obj(),
- value.obj(), static_cast<jint>(notification.flags));
-}
-
-static jobject MakeJObject(JNIEnv* env, jobject inst,
- const nt::LogMessage& msg) {
- static jmethodID constructor = env->GetMethodID(
- logMessageCls, "<init>",
- "(Ledu/wpi/first/networktables/NetworkTableInstance;IILjava/lang/"
- "String;ILjava/lang/String;)V");
+ logMessageCls, "<init>", "(ILjava/lang/String;ILjava/lang/String;)V");
JLocal<jstring> filename{env, MakeJString(env, msg.filename)};
JLocal<jstring> message{env, MakeJString(env, msg.message)};
- return env->NewObject(logMessageCls, constructor, inst,
- static_cast<jint>(msg.logger),
+ return env->NewObject(logMessageCls, constructor,
static_cast<jint>(msg.level), filename.obj(),
static_cast<jint>(msg.line), message.obj());
}
static jobject MakeJObject(JNIEnv* env, jobject inst,
- const nt::RpcAnswer& answer) {
+ const nt::TopicInfo& info) {
+ static jmethodID constructor = env->GetMethodID(
+ topicInfoCls, "<init>",
+ "(Ledu/wpi/first/networktables/"
+ "NetworkTableInstance;ILjava/lang/String;ILjava/lang/String;)V");
+ JLocal<jstring> name{env, MakeJString(env, info.name)};
+ JLocal<jstring> typeStr{env, MakeJString(env, info.type_str)};
+ return env->NewObject(topicInfoCls, constructor, inst,
+ static_cast<jint>(info.topic), name.obj(),
+ static_cast<jint>(info.type), typeStr.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, jobject inst,
+ const nt::ValueEventData& data) {
static jmethodID constructor =
- env->GetMethodID(rpcAnswerCls, "<init>",
- "(Ledu/wpi/first/networktables/"
- "NetworkTableInstance;IILjava/lang/String;[B"
- "Ledu/wpi/first/networktables/ConnectionInfo;)V");
- JLocal<jstring> name{env, MakeJString(env, answer.name)};
- JLocal<jbyteArray> params{env, MakeJByteArray(env, answer.params)};
- JLocal<jobject> conn{env, MakeJObject(env, answer.conn)};
+ env->GetMethodID(valueEventDataCls, "<init>",
+ "(Ledu/wpi/first/networktables/NetworkTableInstance;II"
+ "Ledu/wpi/first/networktables/NetworkTableValue;)V");
+ JLocal<jobject> value{env, MakeJValue(env, data.value)};
+ return env->NewObject(valueEventDataCls, constructor, inst,
+ static_cast<jint>(data.topic),
+ static_cast<jint>(data.subentry), value.obj());
+}
+
+static jobject MakeJObject(JNIEnv* env, const nt::TimeSyncEventData& data) {
+ static jmethodID constructor =
+ env->GetMethodID(timeSyncEventDataCls, "<init>", "(JJZ)V");
+ return env->NewObject(timeSyncEventDataCls, constructor,
+ static_cast<jlong>(data.serverTimeOffset),
+ static_cast<jlong>(data.rtt2),
+ static_cast<jboolean>(data.valid));
+}
+
+static jobject MakeJObject(JNIEnv* env, jobject inst, const nt::Event& event) {
+ static jmethodID constructor =
+ env->GetMethodID(eventCls, "<init>",
+ "(Ledu/wpi/first/networktables/NetworkTableInstance;II"
+ "Ledu/wpi/first/networktables/ConnectionInfo;"
+ "Ledu/wpi/first/networktables/TopicInfo;"
+ "Ledu/wpi/first/networktables/ValueEventData;"
+ "Ledu/wpi/first/networktables/LogMessage;"
+ "Ledu/wpi/first/networktables/TimeSyncEventData;)V");
+ JLocal<jobject> connInfo{env, nullptr};
+ JLocal<jobject> topicInfo{env, nullptr};
+ JLocal<jobject> valueData{env, nullptr};
+ JLocal<jobject> logMessage{env, nullptr};
+ JLocal<jobject> timeSyncData{env, nullptr};
+ if (auto v = event.GetConnectionInfo()) {
+ connInfo = JLocal<jobject>{env, MakeJObject(env, *v)};
+ } else if (auto v = event.GetTopicInfo()) {
+ topicInfo = JLocal<jobject>{env, MakeJObject(env, inst, *v)};
+ } else if (auto v = event.GetValueEventData()) {
+ valueData = JLocal<jobject>{env, MakeJObject(env, inst, *v)};
+ } else if (auto v = event.GetLogMessage()) {
+ logMessage = JLocal<jobject>{env, MakeJObject(env, *v)};
+ } else if (auto v = event.GetTimeSyncEventData()) {
+ timeSyncData = JLocal<jobject>{env, MakeJObject(env, *v)};
+ }
return env->NewObject(
- rpcAnswerCls, constructor, inst, static_cast<jint>(answer.entry),
- static_cast<jint>(answer.call), name.obj(), params.obj(), conn.obj());
+ eventCls, constructor, inst, static_cast<jint>(event.listener),
+ static_cast<jint>(event.flags), connInfo.obj(), topicInfo.obj(),
+ valueData.obj(), logMessage.obj(), timeSyncData.obj());
}
-static jobjectArray MakeJObject(
- JNIEnv* env, jobject inst,
- wpi::span<const nt::ConnectionNotification> arr) {
- jobjectArray jarr =
- env->NewObjectArray(arr.size(), connectionNotificationCls, nullptr);
+static jobjectArray MakeJObject(JNIEnv* env, std::span<const nt::Value> arr) {
+ jobjectArray jarr = env->NewObjectArray(arr.size(), valueCls, nullptr);
if (!jarr) {
return nullptr;
}
for (size_t i = 0; i < arr.size(); ++i) {
- JLocal<jobject> elem{env, MakeJObject(env, inst, arr[i])};
+ JLocal<jobject> elem{env, MakeJValue(env, arr[i])};
env->SetObjectArrayElement(jarr, i, elem.obj());
}
return jarr;
}
static jobjectArray MakeJObject(JNIEnv* env, jobject inst,
- wpi::span<const nt::EntryNotification> arr) {
- jobjectArray jarr =
- env->NewObjectArray(arr.size(), entryNotificationCls, nullptr);
- if (!jarr) {
- return nullptr;
- }
- for (size_t i = 0; i < arr.size(); ++i) {
- JLocal<jobject> elem{env, MakeJObject(env, inst, arr[i])};
- env->SetObjectArrayElement(jarr, i, elem.obj());
- }
- return jarr;
-}
-
-static jobjectArray MakeJObject(JNIEnv* env, jobject inst,
- wpi::span<const nt::LogMessage> arr) {
- jobjectArray jarr = env->NewObjectArray(arr.size(), logMessageCls, nullptr);
- if (!jarr) {
- return nullptr;
- }
- for (size_t i = 0; i < arr.size(); ++i) {
- JLocal<jobject> elem{env, MakeJObject(env, inst, arr[i])};
- env->SetObjectArrayElement(jarr, i, elem.obj());
- }
- return jarr;
-}
-
-static jobjectArray MakeJObject(JNIEnv* env, jobject inst,
- wpi::span<const nt::RpcAnswer> arr) {
- jobjectArray jarr = env->NewObjectArray(arr.size(), rpcAnswerCls, nullptr);
+ std::span<const nt::Event> arr) {
+ jobjectArray jarr = env->NewObjectArray(arr.size(), eventCls, nullptr);
if (!jarr) {
return nullptr;
}
@@ -424,24 +426,7 @@
nullPointerEx.Throw(env, "key cannot be null");
return false;
}
- return nt::GetEntry(inst, JStringRef{env, key}.str());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getEntries
- * Signature: (ILjava/lang/String;I)[I
- */
-JNIEXPORT jintArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntries
- (JNIEnv* env, jclass, jint inst, jstring prefix, jint types)
-{
- if (!prefix) {
- nullPointerEx.Throw(env, "prefix cannot be null");
- return nullptr;
- }
- return MakeJIntArray(
- env, nt::GetEntries(inst, JStringRef{env, prefix}.str(), types));
+ return nt::GetEntry(inst, JStringRef{env, key});
}
/*
@@ -482,183 +467,504 @@
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setBoolean
- * Signature: (IJZZ)Z
+ * Method: getTopics
+ * Signature: (ILjava/lang/String;I)[I
*/
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setBoolean
- (JNIEnv*, jclass, jint entry, jlong time, jboolean value, jboolean force)
+JNIEXPORT jintArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopics
+ (JNIEnv* env, jclass, jint inst, jstring prefix, jint types)
{
- if (force) {
- nt::SetEntryTypeValue(entry,
- nt::Value::MakeBoolean(value != JNI_FALSE, time));
- return JNI_TRUE;
+ if (!prefix) {
+ nullPointerEx.Throw(env, "prefix cannot be null");
+ return nullptr;
}
- return nt::SetEntryValue(entry,
- nt::Value::MakeBoolean(value != JNI_FALSE, time));
+ auto arr = nt::GetTopics(inst, JStringRef{env, prefix}.str(), types);
+ return MakeJIntArray(env, arr);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDouble
- * Signature: (IJDZ)Z
+ * Method: getTopicsStr
+ * Signature: (ILjava/lang/String;[Ljava/lang/Object;)[I
*/
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDouble
- (JNIEnv*, jclass, jint entry, jlong time, jdouble value, jboolean force)
+JNIEXPORT jintArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicsStr
+ (JNIEnv* env, jclass, jint inst, jstring prefix, jobjectArray types)
{
- if (force) {
- nt::SetEntryTypeValue(entry, nt::Value::MakeDouble(value, time));
- return JNI_TRUE;
+ if (!prefix) {
+ nullPointerEx.Throw(env, "prefix cannot be null");
+ return nullptr;
}
- return nt::SetEntryValue(entry, nt::Value::MakeDouble(value, time));
+ if (!types) {
+ nullPointerEx.Throw(env, "types cannot be null");
+ return nullptr;
+ }
+
+ int len = env->GetArrayLength(types);
+ std::vector<std::string> typeStrData;
+ std::vector<std::string_view> typeStrs;
+ typeStrs.reserve(len);
+ for (int i = 0; i < len; ++i) {
+ JLocal<jstring> elem{
+ env, static_cast<jstring>(env->GetObjectArrayElement(types, i))};
+ if (!elem) {
+ nullPointerEx.Throw(env, "null string in types");
+ return nullptr;
+ }
+ typeStrData.emplace_back(JStringRef{env, elem}.str());
+ typeStrs.emplace_back(typeStrData.back());
+ }
+
+ auto arr = nt::GetTopics(inst, JStringRef{env, prefix}.str(), typeStrs);
+ return MakeJIntArray(env, arr);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setString
- * Signature: (IJLjava/lang/String;Z)Z
+ * Method: getTopicInfos
+ * Signature: (Ljava/lang/Object;ILjava/lang/String;I)[Ljava/lang/Object;
*/
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setString
- (JNIEnv* env, jclass, jint entry, jlong time, jstring value, jboolean force)
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicInfos
+ (JNIEnv* env, jclass, jobject instObject, jint inst, jstring prefix,
+ jint types)
{
- if (!value) {
- nullPointerEx.Throw(env, "value cannot be null");
- return false;
+ if (!prefix) {
+ nullPointerEx.Throw(env, "prefix cannot be null");
+ return nullptr;
}
- if (force) {
- nt::SetEntryTypeValue(
- entry, nt::Value::MakeString(JStringRef{env, value}.str(), time));
- return JNI_TRUE;
+ auto arr = nt::GetTopicInfo(inst, JStringRef{env, prefix}.str(), types);
+ jobjectArray jarr = env->NewObjectArray(arr.size(), topicInfoCls, nullptr);
+ if (!jarr) {
+ return nullptr;
}
- return nt::SetEntryValue(
- entry, nt::Value::MakeString(JStringRef{env, value}.str(), time));
+ for (size_t i = 0; i < arr.size(); ++i) {
+ JLocal<jobject> jelem{env, MakeJObject(env, instObject, arr[i])};
+ env->SetObjectArrayElement(jarr, i, jelem);
+ }
+ return jarr;
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setRaw
- * Signature: (IJ[BZ)Z
+ * Method: getTopicInfosStr
+ * Signature: (Ljava/lang/Object;ILjava/lang/String;[Ljava/lang/Object;)[Ljava/lang/Object;
*/
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setRaw__IJ_3BZ
- (JNIEnv* env, jclass, jint entry, jlong time, jbyteArray value,
- jboolean force)
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicInfosStr
+ (JNIEnv* env, jclass, jobject instObject, jint inst, jstring prefix,
+ jobjectArray types)
{
- if (!value) {
- nullPointerEx.Throw(env, "value cannot be null");
- return false;
+ if (!prefix) {
+ nullPointerEx.Throw(env, "prefix cannot be null");
+ return nullptr;
}
- auto v = FromJavaRaw(env, value, time);
- if (!v) {
- return false;
+ if (!types) {
+ nullPointerEx.Throw(env, "types cannot be null");
+ return nullptr;
}
- if (force) {
- nt::SetEntryTypeValue(entry, v);
- return JNI_TRUE;
+
+ int len = env->GetArrayLength(types);
+ std::vector<std::string> typeStrData;
+ std::vector<std::string_view> typeStrs;
+ typeStrs.reserve(len);
+ for (int i = 0; i < len; ++i) {
+ JLocal<jstring> elem{
+ env, static_cast<jstring>(env->GetObjectArrayElement(types, i))};
+ if (!elem) {
+ nullPointerEx.Throw(env, "null string in types");
+ return nullptr;
+ }
+ typeStrData.emplace_back(JStringRef{env, elem}.str());
+ typeStrs.emplace_back(typeStrData.back());
}
- return nt::SetEntryValue(entry, v);
+
+ auto arr = nt::GetTopicInfo(inst, JStringRef{env, prefix}.str(), typeStrs);
+ jobjectArray jarr = env->NewObjectArray(arr.size(), topicInfoCls, nullptr);
+ if (!jarr) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < arr.size(); ++i) {
+ JLocal<jobject> jelem{env, MakeJObject(env, instObject, arr[i])};
+ env->SetObjectArrayElement(jarr, i, jelem);
+ }
+ return jarr;
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setRaw
- * Signature: (IJLjava/lang/Object;IZ)Z
+ * Method: getTopic
+ * Signature: (ILjava/lang/String;)I
*/
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setRaw__IJLjava_nio_ByteBuffer_2IZ
- (JNIEnv* env, jclass, jint entry, jlong time, jobject value, jint len,
- jboolean force)
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopic
+ (JNIEnv* env, jclass, jint inst, jstring name)
{
- if (!value) {
- nullPointerEx.Throw(env, "value cannot be null");
- return false;
- }
- auto v = FromJavaRawBB(env, value, len, time);
- if (!v) {
- return false;
- }
- if (force) {
- nt::SetEntryTypeValue(entry, v);
- return JNI_TRUE;
- }
- return nt::SetEntryValue(entry, v);
+ return nt::GetTopic(inst, JStringRef{env, name});
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setBooleanArray
- * Signature: (IJ[ZZ)Z
+ * Method: getTopicName
+ * Signature: (I)Ljava/lang/String;
*/
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setBooleanArray
- (JNIEnv* env, jclass, jint entry, jlong time, jbooleanArray value,
- jboolean force)
+JNIEXPORT jstring JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicName
+ (JNIEnv* env, jclass, jint topic)
{
- if (!value) {
- nullPointerEx.Throw(env, "value cannot be null");
- return false;
- }
- auto v = FromJavaBooleanArray(env, value, time);
- if (!v) {
- return false;
- }
- if (force) {
- nt::SetEntryTypeValue(entry, v);
- return JNI_TRUE;
- }
- return nt::SetEntryValue(entry, v);
+ return MakeJString(env, nt::GetTopicName(topic));
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDoubleArray
- * Signature: (IJ[DZ)Z
+ * Method: getTopicType
+ * Signature: (I)I
*/
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDoubleArray
- (JNIEnv* env, jclass, jint entry, jlong time, jdoubleArray value,
- jboolean force)
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicType
+ (JNIEnv*, jclass, jint topic)
{
- if (!value) {
- nullPointerEx.Throw(env, "value cannot be null");
- return false;
- }
- auto v = FromJavaDoubleArray(env, value, time);
- if (!v) {
- return false;
- }
- if (force) {
- nt::SetEntryTypeValue(entry, v);
- return JNI_TRUE;
- }
- return nt::SetEntryValue(entry, v);
+ return nt::GetTopicType(topic);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setStringArray
- * Signature: (IJ[Ljava/lang/Object;Z)Z
+ * Method: setTopicPersistent
+ * Signature: (IZ)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setTopicPersistent
+ (JNIEnv*, jclass, jint topic, jboolean value)
+{
+ nt::SetTopicPersistent(topic, value);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getTopicPersistent
+ * Signature: (I)Z
*/
JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setStringArray
- (JNIEnv* env, jclass, jint entry, jlong time, jobjectArray value,
- jboolean force)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicPersistent
+ (JNIEnv*, jclass, jint topic)
{
- if (!value) {
- nullPointerEx.Throw(env, "value cannot be null");
- return false;
+ return nt::GetTopicPersistent(topic);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: setTopicRetained
+ * Signature: (IZ)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setTopicRetained
+ (JNIEnv*, jclass, jint topic, jboolean value)
+{
+ nt::SetTopicRetained(topic, value);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getTopicRetained
+ * Signature: (I)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicRetained
+ (JNIEnv*, jclass, jint topic)
+{
+ return nt::GetTopicRetained(topic);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getTopicTypeString
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicTypeString
+ (JNIEnv* env, jclass, jint topic)
+{
+ return MakeJString(env, nt::GetTopicTypeString(topic));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getTopicExists
+ * Signature: (I)Z
+ */
+JNIEXPORT jboolean JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicExists
+ (JNIEnv*, jclass, jint topic)
+{
+ return nt::GetTopicExists(topic);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getTopicProperty
+ * Signature: (ILjava/lang/String;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicProperty
+ (JNIEnv* env, jclass, jint topic, jstring name)
+{
+ return MakeJString(env,
+ nt::GetTopicProperty(topic, JStringRef{env, name}).dump());
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: setTopicProperty
+ * Signature: (ILjava/lang/String;Ljava/lang/String;)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setTopicProperty
+ (JNIEnv* env, jclass, jint topic, jstring name, jstring value)
+{
+ wpi::json j;
+ try {
+ j = wpi::json::parse(JStringRef{env, value});
+ } catch (wpi::json::parse_error& err) {
+ illegalArgEx.Throw(
+ env, fmt::format("could not parse value JSON: {}", err.what()));
+ return;
}
- auto v = FromJavaStringArray(env, value, time);
- if (!v) {
- return false;
+ nt::SetTopicProperty(topic, JStringRef{env, name}, j);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: deleteTopicProperty
+ * Signature: (ILjava/lang/String;)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_deleteTopicProperty
+ (JNIEnv* env, jclass, jint topic, jstring name)
+{
+ nt::DeleteTopicProperty(topic, JStringRef{env, name});
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getTopicProperties
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicProperties
+ (JNIEnv* env, jclass, jint topic)
+{
+ return MakeJString(env, nt::GetTopicProperties(topic).dump());
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: setTopicProperties
+ * Signature: (ILjava/lang/String;)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_setTopicProperties
+ (JNIEnv* env, jclass, jint topic, jstring properties)
+{
+ wpi::json j;
+ try {
+ j = wpi::json::parse(JStringRef{env, properties});
+ } catch (wpi::json::parse_error& err) {
+ illegalArgEx.Throw(
+ env, fmt::format("could not parse properties JSON: {}", err.what()));
+ return;
}
- if (force) {
- nt::SetEntryTypeValue(entry, v);
- return JNI_TRUE;
+ if (!j.is_object()) {
+ illegalArgEx.Throw(env, "properties is not a JSON object");
+ return;
}
- return nt::SetEntryValue(entry, v);
+ nt::SetTopicProperties(topic, j);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: subscribe
+ * Signature: (IILjava/lang/String;Ljava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_subscribe
+ (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr, jobject options)
+{
+ return nt::Subscribe(topic, static_cast<NT_Type>(type),
+ JStringRef{env, typeStr},
+ FromJavaPubSubOptions(env, options));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: unsubscribe
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_unsubscribe
+ (JNIEnv*, jclass, jint sub)
+{
+ nt::Unsubscribe(sub);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: publish
+ * Signature: (IILjava/lang/String;Ljava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_publish
+ (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr, jobject options)
+{
+ return nt::Publish(topic, static_cast<NT_Type>(type),
+ JStringRef{env, typeStr},
+ FromJavaPubSubOptions(env, options));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: publishEx
+ * Signature: (IILjava/lang/String;Ljava/lang/String;Ljava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_publishEx
+ (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr,
+ jstring properties, jobject options)
+{
+ wpi::json j;
+ try {
+ j = wpi::json::parse(JStringRef{env, properties});
+ } catch (wpi::json::parse_error& err) {
+ illegalArgEx.Throw(
+ env, fmt::format("could not parse properties JSON: {}", err.what()));
+ return 0;
+ }
+ if (!j.is_object()) {
+ illegalArgEx.Throw(env, "properties is not a JSON object");
+ return 0;
+ }
+ return nt::PublishEx(topic, static_cast<NT_Type>(type),
+ JStringRef{env, typeStr}, j,
+ FromJavaPubSubOptions(env, options));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: unpublish
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_unpublish
+ (JNIEnv*, jclass, jint pubentry)
+{
+ nt::Unpublish(pubentry);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getEntryImpl
+ * Signature: (IILjava/lang/String;Ljava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryImpl
+ (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr, jobject options)
+{
+ return nt::GetEntry(topic, static_cast<NT_Type>(type),
+ JStringRef{env, typeStr},
+ FromJavaPubSubOptions(env, options));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: releaseEntry
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_releaseEntry
+ (JNIEnv*, jclass, jint entry)
+{
+ nt::ReleaseEntry(entry);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: release
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_release
+ (JNIEnv*, jclass, jint pubsubentry)
+{
+ nt::Release(pubsubentry);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: getTopicFromHandle
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicFromHandle
+ (JNIEnv*, jclass, jint pubsubentry)
+{
+ return nt::GetTopicFromHandle(pubsubentry);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: subscribeMultiple
+ * Signature: (I[Ljava/lang/Object;Ljava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_subscribeMultiple
+ (JNIEnv* env, jclass, jint inst, jobjectArray prefixes, jobject options)
+{
+ if (!prefixes) {
+ nullPointerEx.Throw(env, "prefixes cannot be null");
+ return {};
+ }
+ int len = env->GetArrayLength(prefixes);
+
+ std::vector<std::string> prefixStrings;
+ std::vector<std::string_view> prefixStringViews;
+ prefixStrings.reserve(len);
+ prefixStringViews.reserve(len);
+ for (int i = 0; i < len; ++i) {
+ JLocal<jstring> elem{
+ env, static_cast<jstring>(env->GetObjectArrayElement(prefixes, i))};
+ if (!elem) {
+ nullPointerEx.Throw(env, "null string in prefixes");
+ return {};
+ }
+ prefixStrings.emplace_back(JStringRef{env, elem}.str());
+ prefixStringViews.emplace_back(prefixStrings.back());
+ }
+
+ return nt::SubscribeMultiple(inst, prefixStringViews,
+ FromJavaPubSubOptions(env, options));
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: unsubscribeMultiple
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_unsubscribeMultiple
+ (JNIEnv*, jclass, jint sub)
+{
+ nt::UnsubscribeMultiple(sub);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: readQueueValue
+ * Signature: (I)[Ljava/lang/Object;
+ */
+JNIEXPORT jobjectArray JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readQueueValue
+ (JNIEnv* env, jclass, jint subentry)
+{
+ return MakeJObject(env, nt::ReadQueueValue(subentry));
}
/*
@@ -670,231 +976,7 @@
Java_edu_wpi_first_networktables_NetworkTablesJNI_getValue
(JNIEnv* env, jclass, jint entry)
{
- auto val = nt::GetEntryValue(entry);
- return MakeJValue(env, val.get());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getBoolean
- * Signature: (IZ)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getBoolean
- (JNIEnv*, jclass, jint entry, jboolean defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsBoolean()) {
- return defaultValue;
- }
- return val->GetBoolean();
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getDouble
- * Signature: (ID)D
- */
-JNIEXPORT jdouble JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getDouble
- (JNIEnv*, jclass, jint entry, jdouble defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsDouble()) {
- return defaultValue;
- }
- return val->GetDouble();
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getString
- * Signature: (ILjava/lang/String;)Ljava/lang/String;
- */
-JNIEXPORT jstring JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getString
- (JNIEnv* env, jclass, jint entry, jstring defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsString()) {
- return defaultValue;
- }
- return MakeJString(env, val->GetString());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getRaw
- * Signature: (I[B)[B
- */
-JNIEXPORT jbyteArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getRaw
- (JNIEnv* env, jclass, jint entry, jbyteArray defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsRaw()) {
- return defaultValue;
- }
- return MakeJByteArray(env, val->GetRaw());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getBooleanArray
- * Signature: (I[Z)[Z
- */
-JNIEXPORT jbooleanArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getBooleanArray
- (JNIEnv* env, jclass, jint entry, jbooleanArray defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsBooleanArray()) {
- return defaultValue;
- }
- return MakeJBooleanArray(env, val->GetBooleanArray());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getDoubleArray
- * Signature: (I[D)[D
- */
-JNIEXPORT jdoubleArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getDoubleArray
- (JNIEnv* env, jclass, jint entry, jdoubleArray defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsDoubleArray()) {
- return defaultValue;
- }
- return MakeJDoubleArray(env, val->GetDoubleArray());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getStringArray
- * Signature: (I[Ljava/lang/Object;)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getStringArray
- (JNIEnv* env, jclass, jint entry, jobjectArray defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsStringArray()) {
- return defaultValue;
- }
- return MakeJStringArray(env, val->GetStringArray());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDefaultBoolean
- * Signature: (IJZ)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultBoolean
- (JNIEnv*, jclass, jint entry, jlong time, jboolean defaultValue)
-{
- return nt::SetDefaultEntryValue(
- entry, nt::Value::MakeBoolean(defaultValue != JNI_FALSE, time));
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDefaultDouble
- * Signature: (IJD)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultDouble
- (JNIEnv*, jclass, jint entry, jlong time, jdouble defaultValue)
-{
- return nt::SetDefaultEntryValue(entry,
- nt::Value::MakeDouble(defaultValue, time));
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDefaultString
- * Signature: (IJLjava/lang/String;)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultString
- (JNIEnv* env, jclass, jint entry, jlong time, jstring defaultValue)
-{
- if (!defaultValue) {
- nullPointerEx.Throw(env, "defaultValue cannot be null");
- return false;
- }
- return nt::SetDefaultEntryValue(
- entry, nt::Value::MakeString(JStringRef{env, defaultValue}.str(), time));
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDefaultRaw
- * Signature: (IJ[B)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultRaw
- (JNIEnv* env, jclass, jint entry, jlong time, jbyteArray defaultValue)
-{
- if (!defaultValue) {
- nullPointerEx.Throw(env, "defaultValue cannot be null");
- return false;
- }
- auto v = FromJavaRaw(env, defaultValue, time);
- return nt::SetDefaultEntryValue(entry, v);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDefaultBooleanArray
- * Signature: (IJ[Z)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultBooleanArray
- (JNIEnv* env, jclass, jint entry, jlong time, jbooleanArray defaultValue)
-{
- if (!defaultValue) {
- nullPointerEx.Throw(env, "defaultValue cannot be null");
- return false;
- }
- auto v = FromJavaBooleanArray(env, defaultValue, time);
- return nt::SetDefaultEntryValue(entry, v);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDefaultDoubleArray
- * Signature: (IJ[D)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultDoubleArray
- (JNIEnv* env, jclass, jint entry, jlong time, jdoubleArray defaultValue)
-{
- if (!defaultValue) {
- nullPointerEx.Throw(env, "defaultValue cannot be null");
- return false;
- }
- auto v = FromJavaDoubleArray(env, defaultValue, time);
- return nt::SetDefaultEntryValue(entry, v);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setDefaultStringArray
- * Signature: (IJ[Ljava/lang/Object;)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setDefaultStringArray
- (JNIEnv* env, jclass, jint entry, jlong time, jobjectArray defaultValue)
-{
- if (!defaultValue) {
- nullPointerEx.Throw(env, "defaultValue cannot be null");
- return false;
- }
- auto v = FromJavaStringArray(env, defaultValue, time);
- return nt::SetDefaultEntryValue(entry, v);
+ return MakeJValue(env, nt::GetEntryValue(entry));
}
/*
@@ -923,503 +1005,108 @@
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: deleteEntry
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_deleteEntry
- (JNIEnv*, jclass, jint entry)
-{
- nt::DeleteEntry(entry);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: deleteAllEntries
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_deleteAllEntries
- (JNIEnv*, jclass, jint inst)
-{
- nt::DeleteAllEntries(inst);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getEntryInfoHandle
+ * Method: getTopicInfo
* Signature: (Ljava/lang/Object;I)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryInfoHandle
- (JNIEnv* env, jclass, jobject inst, jint entry)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getTopicInfo
+ (JNIEnv* env, jclass, jobject inst, jint topic)
{
- return MakeJObject(env, inst, nt::GetEntryInfo(entry));
+ return MakeJObject(env, inst, nt::GetTopicInfo(topic));
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getEntryInfo
- * Signature: (Ljava/lang/Object;ILjava/lang/String;I)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryInfo
- (JNIEnv* env, jclass, jobject instObject, jint inst, jstring prefix,
- jint types)
-{
- if (!prefix) {
- nullPointerEx.Throw(env, "prefix cannot be null");
- return nullptr;
- }
- auto arr = nt::GetEntryInfo(inst, JStringRef{env, prefix}.str(), types);
- jobjectArray jarr = env->NewObjectArray(arr.size(), entryInfoCls, nullptr);
- if (!jarr) {
- return nullptr;
- }
- for (size_t i = 0; i < arr.size(); ++i) {
- JLocal<jobject> jelem{env, MakeJObject(env, instObject, arr[i])};
- env->SetObjectArrayElement(jarr, i, jelem);
- }
- return jarr;
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: createEntryListenerPoller
+ * Method: createListenerPoller
* Signature: (I)I
*/
JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_createEntryListenerPoller
+Java_edu_wpi_first_networktables_NetworkTablesJNI_createListenerPoller
(JNIEnv*, jclass, jint inst)
{
- return nt::CreateEntryListenerPoller(inst);
+ return nt::CreateListenerPoller(inst);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: destroyEntryListenerPoller
+ * Method: destroyListenerPoller
* Signature: (I)V
*/
JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyEntryListenerPoller
+Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyListenerPoller
(JNIEnv*, jclass, jint poller)
{
- nt::DestroyEntryListenerPoller(poller);
+ nt::DestroyListenerPoller(poller);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: addPolledEntryListener
- * Signature: (ILjava/lang/String;I)I
+ * Method: addListener
+ * Signature: (I[Ljava/lang/Object;I)I
*/
JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledEntryListener__ILjava_lang_String_2I
- (JNIEnv* env, jclass, jint poller, jstring prefix, jint flags)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_addListener__I_3Ljava_lang_String_2I
+ (JNIEnv* env, jclass, jint poller, jobjectArray prefixes, jint flags)
{
- if (!prefix) {
- nullPointerEx.Throw(env, "prefix cannot be null");
+ if (!prefixes) {
+ nullPointerEx.Throw(env, "prefixes cannot be null");
return 0;
}
- return nt::AddPolledEntryListener(poller, JStringRef{env, prefix}.str(),
- flags);
+
+ size_t len = env->GetArrayLength(prefixes);
+ std::vector<std::string> arr;
+ std::vector<std::string_view> arrview;
+ arr.reserve(len);
+ arrview.reserve(len);
+ for (size_t i = 0; i < len; ++i) {
+ JLocal<jstring> elem{
+ env, static_cast<jstring>(env->GetObjectArrayElement(prefixes, i))};
+ if (!elem) {
+ nullPointerEx.Throw(env, "prefixes cannot contain null");
+ return 0;
+ }
+ arr.emplace_back(JStringRef{env, elem}.str());
+ // this is safe because of the reserve (so arr elements won't move)
+ arrview.emplace_back(arr.back());
+ }
+
+ return nt::AddPolledListener(poller, arrview, flags);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: addPolledEntryListener
+ * Method: addListener
* Signature: (III)I
*/
JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledEntryListener__III
- (JNIEnv* env, jclass, jint poller, jint entry, jint flags)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_addListener__III
+ (JNIEnv* env, jclass, jint poller, jint handle, jint flags)
{
- return nt::AddPolledEntryListener(poller, entry, flags);
+ return nt::AddPolledListener(poller, handle, flags);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollEntryListener
+ * Method: readListenerQueue
* Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
*/
JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollEntryListener
+Java_edu_wpi_first_networktables_NetworkTablesJNI_readListenerQueue
(JNIEnv* env, jclass, jobject inst, jint poller)
{
- auto events = nt::PollEntryListener(poller);
- if (events.empty()) {
- interruptedEx.Throw(env, "PollEntryListener interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, events);
+ return MakeJObject(env, inst, nt::ReadListenerQueue(poller));
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollEntryListenerTimeout
- * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollEntryListenerTimeout
- (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
-{
- bool timed_out = false;
- auto events = nt::PollEntryListener(poller, timeout, &timed_out);
- if (events.empty() && !timed_out) {
- interruptedEx.Throw(env, "PollEntryListener interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, events);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: cancelPollEntryListener
+ * Method: removeListener
* Signature: (I)V
*/
JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollEntryListener
- (JNIEnv*, jclass, jint poller)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_removeListener
+ (JNIEnv*, jclass, jint topicListener)
{
- nt::CancelPollEntryListener(poller);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: removeEntryListener
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_removeEntryListener
- (JNIEnv*, jclass, jint entryListenerUid)
-{
- nt::RemoveEntryListener(entryListenerUid);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: waitForEntryListenerQueue
- * Signature: (ID)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForEntryListenerQueue
- (JNIEnv*, jclass, jint inst, jdouble timeout)
-{
- return nt::WaitForEntryListenerQueue(inst, timeout);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: createConnectionListenerPoller
- * Signature: (I)I
- */
-JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_createConnectionListenerPoller
- (JNIEnv*, jclass, jint inst)
-{
- return nt::CreateConnectionListenerPoller(inst);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: destroyConnectionListenerPoller
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyConnectionListenerPoller
- (JNIEnv*, jclass, jint poller)
-{
- nt::DestroyConnectionListenerPoller(poller);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: addPolledConnectionListener
- * Signature: (IZ)I
- */
-JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledConnectionListener
- (JNIEnv* env, jclass, jint poller, jboolean immediateNotify)
-{
- return nt::AddPolledConnectionListener(poller, immediateNotify);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollConnectionListener
- * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollConnectionListener
- (JNIEnv* env, jclass, jobject inst, jint poller)
-{
- auto events = nt::PollConnectionListener(poller);
- if (events.empty()) {
- interruptedEx.Throw(env, "PollConnectionListener interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, events);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollConnectionListenerTimeout
- * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollConnectionListenerTimeout
- (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
-{
- bool timed_out = false;
- auto events = nt::PollConnectionListener(poller, timeout, &timed_out);
- if (events.empty() && !timed_out) {
- interruptedEx.Throw(env, "PollConnectionListener interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, events);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: cancelPollConnectionListener
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollConnectionListener
- (JNIEnv*, jclass, jint poller)
-{
- nt::CancelPollConnectionListener(poller);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: removeConnectionListener
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_removeConnectionListener
- (JNIEnv*, jclass, jint connListenerUid)
-{
- nt::RemoveConnectionListener(connListenerUid);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: waitForConnectionListenerQueue
- * Signature: (ID)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForConnectionListenerQueue
- (JNIEnv*, jclass, jint inst, jdouble timeout)
-{
- return nt::WaitForConnectionListenerQueue(inst, timeout);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: createRpcCallPoller
- * Signature: (I)I
- */
-JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_createRpcCallPoller
- (JNIEnv*, jclass, jint inst)
-{
- return nt::CreateRpcCallPoller(inst);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: destroyRpcCallPoller
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyRpcCallPoller
- (JNIEnv*, jclass, jint poller)
-{
- nt::DestroyRpcCallPoller(poller);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: createPolledRpc
- * Signature: (I[BI)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_createPolledRpc
- (JNIEnv* env, jclass, jint entry, jbyteArray def, jint poller)
-{
- if (!def) {
- nullPointerEx.Throw(env, "def cannot be null");
- return;
- }
- nt::CreatePolledRpc(entry, JByteArrayRef{env, def}.str(), poller);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollRpc
- * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollRpc
- (JNIEnv* env, jclass, jobject inst, jint poller)
-{
- auto infos = nt::PollRpc(poller);
- if (infos.empty()) {
- interruptedEx.Throw(env, "PollRpc interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, infos);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollRpcTimeout
- * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollRpcTimeout
- (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
-{
- bool timed_out = false;
- auto infos = nt::PollRpc(poller, timeout, &timed_out);
- if (infos.empty() && !timed_out) {
- interruptedEx.Throw(env, "PollRpc interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, infos);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: cancelPollRpc
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollRpc
- (JNIEnv*, jclass, jint poller)
-{
- nt::CancelPollRpc(poller);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: waitForRpcCallQueue
- * Signature: (ID)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForRpcCallQueue
- (JNIEnv*, jclass, jint inst, jdouble timeout)
-{
- return nt::WaitForRpcCallQueue(inst, timeout);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: postRpcResponse
- * Signature: (II[B)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_postRpcResponse
- (JNIEnv* env, jclass, jint entry, jint call, jbyteArray result)
-{
- if (!result) {
- nullPointerEx.Throw(env, "result cannot be null");
- return false;
- }
- return nt::PostRpcResponse(entry, call, JByteArrayRef{env, result}.str());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: callRpc
- * Signature: (I[B)I
- */
-JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_callRpc
- (JNIEnv* env, jclass, jint entry, jbyteArray params)
-{
- if (!params) {
- nullPointerEx.Throw(env, "params cannot be null");
- return 0;
- }
- return nt::CallRpc(entry, JByteArrayRef{env, params}.str());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getRpcResult
- * Signature: (II)[B
- */
-JNIEXPORT jbyteArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getRpcResult__II
- (JNIEnv* env, jclass, jint entry, jint call)
-{
- std::string result;
- if (!nt::GetRpcResult(entry, call, &result)) {
- return nullptr;
- }
- return MakeJByteArray(env, result);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getRpcResult
- * Signature: (IID)[B
- */
-JNIEXPORT jbyteArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getRpcResult__IID
- (JNIEnv* env, jclass, jint entry, jint call, jdouble timeout)
-{
- std::string result;
- bool timed_out = false;
- if (!nt::GetRpcResult(entry, call, &result, timeout, &timed_out)) {
- return nullptr;
- }
- return MakeJByteArray(env, result);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: cancelRpcResult
- * Signature: (II)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelRpcResult
- (JNIEnv*, jclass, jint entry, jint call)
-{
- nt::CancelRpcResult(entry, call);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: getRpc
- * Signature: (I[B)[B
- */
-JNIEXPORT jbyteArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_getRpc
- (JNIEnv* env, jclass, jint entry, jbyteArray defaultValue)
-{
- auto val = nt::GetEntryValue(entry);
- if (!val || !val->IsRpc()) {
- return defaultValue;
- }
- return MakeJByteArray(env, val->GetRpc());
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setNetworkIdentity
- * Signature: (ILjava/lang/String;)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setNetworkIdentity
- (JNIEnv* env, jclass, jint inst, jstring name)
-{
- if (!name) {
- nullPointerEx.Throw(env, "name cannot be null");
- return;
- }
- nt::SetNetworkIdentity(inst, JStringRef{env, name}.str());
+ nt::RemoveListener(topicListener);
}
/*
@@ -1461,12 +1148,12 @@
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
* Method: startServer
- * Signature: (ILjava/lang/String;Ljava/lang/String;I)V
+ * Signature: (ILjava/lang/String;Ljava/lang/String;II)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_networktables_NetworkTablesJNI_startServer
(JNIEnv* env, jclass, jint inst, jstring persistFilename,
- jstring listenAddress, jint port)
+ jstring listenAddress, jint port3, jint port4)
{
if (!persistFilename) {
nullPointerEx.Throw(env, "persistFilename cannot be null");
@@ -1477,7 +1164,7 @@
return;
}
nt::StartServer(inst, JStringRef{env, persistFilename}.str(),
- JStringRef{env, listenAddress}.c_str(), port);
+ JStringRef{env, listenAddress}.c_str(), port3, port4);
}
/*
@@ -1494,89 +1181,34 @@
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: startClient
- * Signature: (I)V
+ * Method: startClient3
+ * Signature: (ILjava/lang/String;)V
*/
JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient__I
- (JNIEnv*, jclass, jint inst)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient3
+ (JNIEnv* env, jclass, jint inst, jstring identity)
{
- nt::StartClient(inst);
+ if (!identity) {
+ nullPointerEx.Throw(env, "identity cannot be null");
+ return;
+ }
+ nt::StartClient3(inst, JStringRef{env, identity}.str());
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: startClient
- * Signature: (ILjava/lang/String;I)V
+ * Method: startClient4
+ * Signature: (ILjava/lang/String;)V
*/
JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient__ILjava_lang_String_2I
- (JNIEnv* env, jclass, jint inst, jstring serverName, jint port)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient4
+ (JNIEnv* env, jclass, jint inst, jstring identity)
{
- if (!serverName) {
- nullPointerEx.Throw(env, "serverName cannot be null");
+ if (!identity) {
+ nullPointerEx.Throw(env, "identity cannot be null");
return;
}
- nt::StartClient(inst, JStringRef{env, serverName}.c_str(), port);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: startClient
- * Signature: (I[Ljava/lang/Object;[I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient__I_3Ljava_lang_String_2_3I
- (JNIEnv* env, jclass, jint inst, jobjectArray serverNames, jintArray ports)
-{
- if (!serverNames) {
- nullPointerEx.Throw(env, "serverNames cannot be null");
- return;
- }
- if (!ports) {
- nullPointerEx.Throw(env, "ports cannot be null");
- return;
- }
- int len = env->GetArrayLength(serverNames);
- if (len != env->GetArrayLength(ports)) {
- illegalArgEx.Throw(env,
- "serverNames and ports arrays must be the same size");
- return;
- }
- jint* portInts = env->GetIntArrayElements(ports, nullptr);
- if (!portInts) {
- return;
- }
-
- std::vector<std::string> names;
- std::vector<std::pair<std::string_view, unsigned int>> servers;
- names.reserve(len);
- servers.reserve(len);
- for (int i = 0; i < len; ++i) {
- JLocal<jstring> elem{
- env, static_cast<jstring>(env->GetObjectArrayElement(serverNames, i))};
- if (!elem) {
- nullPointerEx.Throw(env, "null string in serverNames");
- return;
- }
- names.emplace_back(JStringRef{env, elem}.str());
- servers.emplace_back(
- std::make_pair(std::string_view{names.back()}, portInts[i]));
- }
- env->ReleaseIntArrayElements(ports, portInts, JNI_ABORT);
- nt::StartClient(inst, servers);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: startClientTeam
- * Signature: (III)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_startClientTeam
- (JNIEnv* env, jclass cls, jint inst, jint team, jint port)
-{
- nt::StartClientTeam(inst, team, port);
+ nt::StartClient4(inst, JStringRef{env, identity}.str());
}
/*
@@ -1692,14 +1324,14 @@
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: setUpdateRate
- * Signature: (ID)V
+ * Method: flushLocal
+ * Signature: (I)V
*/
JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_setUpdateRate
- (JNIEnv*, jclass, jint inst, jdouble interval)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_flushLocal
+ (JNIEnv*, jclass, jint inst)
{
- nt::SetUpdateRate(inst, interval);
+ nt::FlushLocal(inst);
}
/*
@@ -1750,100 +1382,14 @@
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: savePersistent
- * Signature: (ILjava/lang/String;)V
+ * Method: getServerTimeOffset
+ * Signature: (I)Ljava/lang/Object;
*/
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_savePersistent
- (JNIEnv* env, jclass, jint inst, jstring filename)
+JNIEXPORT jobject JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_getServerTimeOffset
+ (JNIEnv* env, jclass, jint inst)
{
- if (!filename) {
- nullPointerEx.Throw(env, "filename cannot be null");
- return;
- }
- const char* err = nt::SavePersistent(inst, JStringRef{env, filename}.str());
- if (err) {
- persistentEx.Throw(env, err);
- }
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: loadPersistent
- * Signature: (ILjava/lang/String;)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_loadPersistent
- (JNIEnv* env, jclass, jint inst, jstring filename)
-{
- if (!filename) {
- nullPointerEx.Throw(env, "filename cannot be null");
- return nullptr;
- }
- std::vector<std::string> warns;
- const char* err = nt::LoadPersistent(
- inst, JStringRef{env, filename}.str(), [&](size_t line, const char* msg) {
- warns.emplace_back(fmt::format("{}: {}", line, msg));
- });
- if (err) {
- persistentEx.Throw(env, err);
- return nullptr;
- }
- return MakeJStringArray(env, warns);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: saveEntries
- * Signature: (ILjava/lang/String;Ljava/lang/String;)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_saveEntries
- (JNIEnv* env, jclass, jint inst, jstring filename, jstring prefix)
-{
- if (!filename) {
- nullPointerEx.Throw(env, "filename cannot be null");
- return;
- }
- if (!prefix) {
- nullPointerEx.Throw(env, "prefix cannot be null");
- return;
- }
- const char* err = nt::SaveEntries(inst, JStringRef{env, filename}.str(),
- JStringRef{env, prefix}.str());
- if (err) {
- persistentEx.Throw(env, err);
- }
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: loadEntries
- * Signature: (ILjava/lang/String;Ljava/lang/String;)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_loadEntries
- (JNIEnv* env, jclass, jint inst, jstring filename, jstring prefix)
-{
- if (!filename) {
- nullPointerEx.Throw(env, "filename cannot be null");
- return nullptr;
- }
- if (!prefix) {
- nullPointerEx.Throw(env, "prefix cannot be null");
- return nullptr;
- }
- std::vector<std::string> warns;
- const char* err = nt::LoadEntries(
- inst, JStringRef{env, filename}.str(), JStringRef{env, prefix}.str(),
- [&](size_t line, const char* msg) {
- warns.emplace_back(fmt::format("{}: {}", line, msg));
- });
- if (err) {
- persistentEx.Throw(env, err);
- return nullptr;
- }
- return MakeJStringArray(env, warns);
+ return MakeJObject(env, nt::GetServerTimeOffset(inst));
}
/*
@@ -1860,109 +1406,65 @@
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: createLoggerPoller
- * Signature: (I)I
+ * Method: startEntryDataLog
+ * Signature: (IJLjava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_createLoggerPoller
- (JNIEnv*, jclass, jint inst)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startEntryDataLog
+ (JNIEnv* env, jclass, jint inst, jlong log, jstring prefix, jstring logPrefix)
{
- return nt::CreateLoggerPoller(inst);
+ return nt::StartEntryDataLog(inst, *reinterpret_cast<wpi::log::DataLog*>(log),
+ JStringRef{env, prefix},
+ JStringRef{env, logPrefix});
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: destroyLoggerPoller
+ * Method: stopEntryDataLog
* Signature: (I)V
*/
JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyLoggerPoller
- (JNIEnv*, jclass, jint poller)
+Java_edu_wpi_first_networktables_NetworkTablesJNI_stopEntryDataLog
+ (JNIEnv*, jclass, jint logger)
{
- nt::DestroyLoggerPoller(poller);
+ nt::StopEntryDataLog(logger);
}
/*
* Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: addPolledLogger
+ * Method: startConnectionDataLog
+ * Signature: (IJLjava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_startConnectionDataLog
+ (JNIEnv* env, jclass, jint inst, jlong log, jstring name)
+{
+ return nt::StartConnectionDataLog(
+ inst, *reinterpret_cast<wpi::log::DataLog*>(log), JStringRef{env, name});
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: stopConnectionDataLog
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL
+Java_edu_wpi_first_networktables_NetworkTablesJNI_stopConnectionDataLog
+ (JNIEnv*, jclass, jint logger)
+{
+ nt::StopConnectionDataLog(logger);
+}
+
+/*
+ * Class: edu_wpi_first_networktables_NetworkTablesJNI
+ * Method: addLogger
* Signature: (III)I
*/
JNIEXPORT jint JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledLogger
+Java_edu_wpi_first_networktables_NetworkTablesJNI_addLogger
(JNIEnv*, jclass, jint poller, jint minLevel, jint maxLevel)
{
return nt::AddPolledLogger(poller, minLevel, maxLevel);
}
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollLogger
- * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollLogger
- (JNIEnv* env, jclass, jobject inst, jint poller)
-{
- auto events = nt::PollLogger(poller);
- if (events.empty()) {
- interruptedEx.Throw(env, "PollLogger interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, events);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: pollLoggerTimeout
- * Signature: (Ljava/lang/Object;ID)[Ljava/lang/Object;
- */
-JNIEXPORT jobjectArray JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_pollLoggerTimeout
- (JNIEnv* env, jclass, jobject inst, jint poller, jdouble timeout)
-{
- bool timed_out = false;
- auto events = nt::PollLogger(poller, timeout, &timed_out);
- if (events.empty() && !timed_out) {
- interruptedEx.Throw(env, "PollLogger interrupted");
- return nullptr;
- }
- return MakeJObject(env, inst, events);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: cancelPollLogger
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollLogger
- (JNIEnv*, jclass, jint poller)
-{
- nt::CancelPollLogger(poller);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: removeLogger
- * Signature: (I)V
- */
-JNIEXPORT void JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_removeLogger
- (JNIEnv*, jclass, jint logger)
-{
- nt::RemoveLogger(logger);
-}
-
-/*
- * Class: edu_wpi_first_networktables_NetworkTablesJNI
- * Method: waitForLoggerQueue
- * Signature: (ID)Z
- */
-JNIEXPORT jboolean JNICALL
-Java_edu_wpi_first_networktables_NetworkTablesJNI_waitForLoggerQueue
- (JNIEnv*, jclass, jint inst, jdouble timeout)
-{
- return nt::WaitForLoggerQueue(inst, timeout);
-}
-
} // extern "C"
diff --git a/ntcore/src/main/native/cpp/net/ClientImpl.cpp b/ntcore/src/main/native/cpp/net/ClientImpl.cpp
new file mode 100644
index 0000000..2efbb29
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/ClientImpl.cpp
@@ -0,0 +1,489 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "ClientImpl.h"
+
+#include <numeric>
+#include <optional>
+#include <string>
+#include <variant>
+
+#include <fmt/format.h>
+#include <wpi/DenseMap.h>
+#include <wpi/Logger.h>
+#include <wpi/raw_ostream.h>
+#include <wpi/timestamp.h>
+
+#include "Handle.h"
+#include "Log.h"
+#include "Message.h"
+#include "NetworkInterface.h"
+#include "PubSubOptions.h"
+#include "WireConnection.h"
+#include "WireDecoder.h"
+#include "WireEncoder.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace nt;
+using namespace nt::net;
+
+static constexpr uint32_t kMinPeriodMs = 5;
+
+// maximum number of times the wire can be not ready to send another
+// transmission before we close the connection
+static constexpr int kWireMaxNotReady = 10;
+
+namespace {
+
+struct PublisherData {
+ NT_Publisher handle;
+ PubSubOptionsImpl options;
+ // in options as double, but copy here as integer; rounded to the nearest
+ // 10 ms
+ uint32_t periodMs;
+ uint64_t nextSendMs{0};
+ std::vector<Value> outValues; // outgoing values
+};
+
+class CImpl : public ServerMessageHandler {
+ public:
+ CImpl(uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated,
+ std::function<void(uint32_t repeatMs)> setPeriodic);
+
+ void ProcessIncomingBinary(std::span<const uint8_t> data);
+ void HandleLocal(std::vector<ClientMessage>&& msgs);
+ bool SendControl(uint64_t curTimeMs);
+ void SendValues(uint64_t curTimeMs);
+ void SendInitialValues();
+ bool CheckNetworkReady();
+
+ // ServerMessageHandler interface
+ void ServerAnnounce(std::string_view name, int64_t id,
+ std::string_view typeStr, const wpi::json& properties,
+ std::optional<int64_t> pubuid) final;
+ void ServerUnannounce(std::string_view name, int64_t id) final;
+ void ServerPropertiesUpdate(std::string_view name, const wpi::json& update,
+ bool ack) final;
+
+ void Publish(NT_Publisher pubHandle, NT_Topic topicHandle,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties, const PubSubOptionsImpl& options);
+ bool Unpublish(NT_Publisher pubHandle, NT_Topic topicHandle);
+ void SetValue(NT_Publisher pubHandle, const Value& value);
+
+ int m_inst;
+ WireConnection& m_wire;
+ wpi::Logger& m_logger;
+ LocalInterface* m_local{nullptr};
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ m_timeSyncUpdated;
+ std::function<void(uint32_t repeatMs)> m_setPeriodic;
+
+ // indexed by publisher index
+ std::vector<std::unique_ptr<PublisherData>> m_publishers;
+
+ // indexed by server-provided topic id
+ wpi::DenseMap<int64_t, NT_Topic> m_topicMap;
+
+ // timestamp handling
+ static constexpr uint32_t kPingIntervalMs = 3000;
+ uint64_t m_nextPingTimeMs{0};
+ uint32_t m_rtt2Us{UINT32_MAX};
+ bool m_haveTimeOffset{false};
+ int64_t m_serverTimeOffsetUs{0};
+
+ // periodic sweep handling
+ uint32_t m_periodMs{kPingIntervalMs + 10};
+ uint64_t m_lastSendMs{0};
+ int m_notReadyCount{0};
+
+ // outgoing queue
+ std::vector<ClientMessage> m_outgoing;
+};
+
+} // namespace
+
+CImpl::CImpl(
+ uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated,
+ std::function<void(uint32_t repeatMs)> setPeriodic)
+ : m_inst{inst},
+ m_wire{wire},
+ m_logger{logger},
+ m_timeSyncUpdated{std::move(timeSyncUpdated)},
+ m_setPeriodic{std::move(setPeriodic)},
+ m_nextPingTimeMs{curTimeMs + kPingIntervalMs} {
+ // immediately send RTT ping
+ auto out = m_wire.SendBinary();
+ auto now = wpi::Now();
+ DEBUG4("Sending initial RTT ping {}", now);
+ WireEncodeBinary(out.Add(), -1, 0, Value::MakeInteger(now));
+ m_wire.Flush();
+ m_setPeriodic(m_periodMs);
+}
+
+void CImpl::ProcessIncomingBinary(std::span<const uint8_t> data) {
+ for (;;) {
+ if (data.empty()) {
+ break;
+ }
+
+ // decode message
+ int64_t id;
+ Value value;
+ std::string error;
+ if (!WireDecodeBinary(&data, &id, &value, &error, -m_serverTimeOffsetUs)) {
+ ERROR("binary decode error: {}", error);
+ break; // FIXME
+ }
+ DEBUG4("BinaryMessage({})", id);
+
+ // handle RTT ping response
+ if (id == -1) {
+ if (!value.IsInteger()) {
+ WARNING("RTT ping response with non-integer type {}",
+ static_cast<int>(value.type()));
+ continue;
+ }
+ DEBUG4("RTT ping response time {} value {}", value.time(),
+ value.GetInteger());
+ int64_t now = wpi::Now();
+ int64_t rtt2 = (now - value.GetInteger()) / 2;
+ if (rtt2 < m_rtt2Us) {
+ m_rtt2Us = rtt2;
+ m_serverTimeOffsetUs = value.server_time() + rtt2 - now;
+ DEBUG3("Time offset: {}", m_serverTimeOffsetUs);
+ m_haveTimeOffset = true;
+ m_timeSyncUpdated(m_serverTimeOffsetUs, m_rtt2Us, true);
+ }
+ continue;
+ }
+
+ // otherwise it's a value message, get the local topic handle for it
+ auto topicIt = m_topicMap.find(id);
+ if (topicIt == m_topicMap.end()) {
+ WARNING("received unknown id {}", id);
+ continue;
+ }
+
+ // pass along to local handler
+ if (m_local) {
+ m_local->NetworkSetValue(topicIt->second, value);
+ }
+ }
+}
+
+void CImpl::HandleLocal(std::vector<ClientMessage>&& msgs) {
+ DEBUG4("HandleLocal()");
+ for (auto&& elem : msgs) {
+ // common case is value
+ if (auto msg = std::get_if<ClientValueMsg>(&elem.contents)) {
+ SetValue(msg->pubHandle, msg->value);
+ // setvalue puts on individual publish outgoing queues
+ } else if (auto msg = std::get_if<PublishMsg>(&elem.contents)) {
+ Publish(msg->pubHandle, msg->topicHandle, msg->name, msg->typeStr,
+ msg->properties, msg->options);
+ m_outgoing.emplace_back(std::move(elem));
+ } else if (auto msg = std::get_if<UnpublishMsg>(&elem.contents)) {
+ if (Unpublish(msg->pubHandle, msg->topicHandle)) {
+ m_outgoing.emplace_back(std::move(elem));
+ }
+ } else {
+ m_outgoing.emplace_back(std::move(elem));
+ }
+ }
+}
+
+bool CImpl::SendControl(uint64_t curTimeMs) {
+ DEBUG4("SendControl({})", curTimeMs);
+
+ // rate limit sends
+ if (curTimeMs < (m_lastSendMs + kMinPeriodMs)) {
+ return false;
+ }
+
+ // start a timestamp RTT ping if it's time to do one
+ if (curTimeMs >= m_nextPingTimeMs) {
+ if (!CheckNetworkReady()) {
+ return false;
+ }
+ auto now = wpi::Now();
+ DEBUG4("Sending RTT ping {}", now);
+ WireEncodeBinary(m_wire.SendBinary().Add(), -1, 0, Value::MakeInteger(now));
+ // drift isn't critical here, so just go from current time
+ m_nextPingTimeMs = curTimeMs + kPingIntervalMs;
+ }
+
+ if (!m_outgoing.empty()) {
+ if (!CheckNetworkReady()) {
+ return false;
+ }
+ auto writer = m_wire.SendText();
+ for (auto&& msg : m_outgoing) {
+ auto& stream = writer.Add();
+ if (!WireEncodeText(stream, msg)) {
+ // shouldn't happen, but just in case...
+ stream << "{}";
+ }
+ }
+ m_outgoing.resize(0);
+ }
+
+ m_lastSendMs = curTimeMs;
+ return true;
+}
+
+void CImpl::SendValues(uint64_t curTimeMs) {
+ DEBUG4("SendValues({})", curTimeMs);
+
+ // can't send value updates until we have a RTT
+ if (!m_haveTimeOffset) {
+ return;
+ }
+
+ // ensure all control messages are sent ahead of value updates
+ if (!SendControl(curTimeMs)) {
+ return;
+ }
+
+ // send any pending updates due to be sent
+ bool checkedNetwork = false;
+ auto writer = m_wire.SendBinary();
+ for (auto&& pub : m_publishers) {
+ if (pub && !pub->outValues.empty() && curTimeMs >= pub->nextSendMs) {
+ for (auto&& val : pub->outValues) {
+ if (!checkedNetwork) {
+ if (!CheckNetworkReady()) {
+ return;
+ }
+ checkedNetwork = true;
+ }
+ DEBUG4("Sending {} value time={} server_time={} st_off={}", pub->handle,
+ val.time(), val.server_time(), m_serverTimeOffsetUs);
+ int64_t time = val.time();
+ if (time != 0) {
+ time += m_serverTimeOffsetUs;
+ }
+ WireEncodeBinary(writer.Add(), Handle{pub->handle}.GetIndex(), time,
+ val);
+ }
+ pub->outValues.resize(0);
+ pub->nextSendMs = curTimeMs + pub->periodMs;
+ }
+ }
+}
+
+void CImpl::SendInitialValues() {
+ DEBUG4("SendInitialValues()");
+
+ // ensure all control messages are sent ahead of value updates
+ if (!SendControl(0)) {
+ return;
+ }
+
+ // only send time=0 values (as we don't have a RTT yet)
+ auto writer = m_wire.SendBinary();
+ for (auto&& pub : m_publishers) {
+ if (pub && !pub->outValues.empty()) {
+ bool sent = false;
+ for (auto&& val : pub->outValues) {
+ if (val.server_time() == 0) {
+ DEBUG4("Sending {} value time={} server_time={}", pub->handle,
+ val.time(), val.server_time());
+ WireEncodeBinary(writer.Add(), Handle{pub->handle}.GetIndex(), 0,
+ val);
+ sent = true;
+ }
+ }
+ if (sent) {
+ std::erase_if(pub->outValues,
+ [](const auto& v) { return v.server_time() == 0; });
+ }
+ }
+ }
+}
+
+bool CImpl::CheckNetworkReady() {
+ if (!m_wire.Ready()) {
+ ++m_notReadyCount;
+ if (m_notReadyCount > kWireMaxNotReady) {
+ m_wire.Disconnect("transmit stalled");
+ }
+ return false;
+ }
+ m_notReadyCount = 0;
+ return true;
+}
+
+void CImpl::Publish(NT_Publisher pubHandle, NT_Topic topicHandle,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptionsImpl& options) {
+ unsigned int index = Handle{pubHandle}.GetIndex();
+ if (index >= m_publishers.size()) {
+ m_publishers.resize(index + 1);
+ }
+ auto& publisher = m_publishers[index];
+ if (!publisher) {
+ publisher = std::make_unique<PublisherData>();
+ }
+ publisher->handle = pubHandle;
+ publisher->options = options;
+ publisher->periodMs = std::lround(options.periodicMs / 10.0) * 10;
+ if (publisher->periodMs < kMinPeriodMs) {
+ publisher->periodMs = kMinPeriodMs;
+ }
+
+ // update period
+ m_periodMs = std::gcd(m_periodMs, publisher->periodMs);
+ if (m_periodMs < kMinPeriodMs) {
+ m_periodMs = kMinPeriodMs;
+ }
+ m_setPeriodic(m_periodMs);
+}
+
+bool CImpl::Unpublish(NT_Publisher pubHandle, NT_Topic topicHandle) {
+ unsigned int index = Handle{pubHandle}.GetIndex();
+ if (index >= m_publishers.size()) {
+ return false;
+ }
+ bool doSend = true;
+ if (m_publishers[index]) {
+ // Look through outgoing queue to see if the publish hasn't been sent yet;
+ // if it hasn't, delete it and don't send the server a message.
+ // The outgoing queue doesn't contain values; those are deleted with the
+ // publisher object.
+ auto it = std::find_if(
+ m_outgoing.begin(), m_outgoing.end(), [&](const auto& elem) {
+ if (auto msg = std::get_if<PublishMsg>(&elem.contents)) {
+ return msg->pubHandle == pubHandle;
+ }
+ return false;
+ });
+ if (it != m_outgoing.end()) {
+ m_outgoing.erase(it);
+ doSend = false;
+ }
+ }
+ m_publishers[index].reset();
+
+ // loop over all publishers to update period
+ m_periodMs = kPingIntervalMs + 10;
+ for (auto&& pub : m_publishers) {
+ if (pub) {
+ m_periodMs = std::gcd(m_periodMs, pub->periodMs);
+ }
+ }
+ if (m_periodMs < kMinPeriodMs) {
+ m_periodMs = kMinPeriodMs;
+ }
+ m_setPeriodic(m_periodMs);
+
+ return doSend;
+}
+
+void CImpl::SetValue(NT_Publisher pubHandle, const Value& value) {
+ DEBUG4("SetValue({}, time={}, server_time={}, st_off={})", pubHandle,
+ value.time(), value.server_time(), m_serverTimeOffsetUs);
+ unsigned int index = Handle{pubHandle}.GetIndex();
+ if (index >= m_publishers.size() || !m_publishers[index]) {
+ return;
+ }
+ auto& publisher = *m_publishers[index];
+ if (publisher.outValues.empty() || publisher.options.sendAll) {
+ publisher.outValues.emplace_back(value);
+ } else {
+ publisher.outValues.back() = value;
+ }
+}
+
+void CImpl::ServerAnnounce(std::string_view name, int64_t id,
+ std::string_view typeStr,
+ const wpi::json& properties,
+ std::optional<int64_t> pubuid) {
+ DEBUG4("ServerAnnounce({}, {}, {})", name, id, typeStr);
+ assert(m_local);
+ NT_Publisher pubHandle{0};
+ if (pubuid) {
+ pubHandle = Handle(m_inst, pubuid.value(), Handle::kPublisher);
+ }
+ m_topicMap[id] =
+ m_local->NetworkAnnounce(name, typeStr, properties, pubHandle);
+}
+
+void CImpl::ServerUnannounce(std::string_view name, int64_t id) {
+ DEBUG4("ServerUnannounce({}, {})", name, id);
+ assert(m_local);
+ m_local->NetworkUnannounce(name);
+ m_topicMap.erase(id);
+}
+
+void CImpl::ServerPropertiesUpdate(std::string_view name,
+ const wpi::json& update, bool ack) {
+ DEBUG4("ServerProperties({}, {}, {})", name, update.dump(), ack);
+ assert(m_local);
+ m_local->NetworkPropertiesUpdate(name, update, ack);
+}
+
+class ClientImpl::Impl final : public CImpl {
+ public:
+ Impl(uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated,
+ std::function<void(uint32_t repeatMs)> setPeriodic)
+ : CImpl{curTimeMs,
+ inst,
+ wire,
+ logger,
+ std::move(timeSyncUpdated),
+ std::move(setPeriodic)} {}
+};
+
+ClientImpl::ClientImpl(
+ uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated,
+ std::function<void(uint32_t repeatMs)> setPeriodic)
+ : m_impl{std::make_unique<Impl>(curTimeMs, inst, wire, logger,
+ std::move(timeSyncUpdated),
+ std::move(setPeriodic))} {}
+
+ClientImpl::~ClientImpl() = default;
+
+void ClientImpl::ProcessIncomingText(std::string_view data) {
+ if (!m_impl->m_local) {
+ return;
+ }
+ WireDecodeText(data, *m_impl, m_impl->m_logger);
+}
+
+void ClientImpl::ProcessIncomingBinary(std::span<const uint8_t> data) {
+ m_impl->ProcessIncomingBinary(data);
+}
+
+void ClientImpl::HandleLocal(std::vector<ClientMessage>&& msgs) {
+ m_impl->HandleLocal(std::move(msgs));
+}
+
+void ClientImpl::SendControl(uint64_t curTimeMs) {
+ m_impl->SendControl(curTimeMs);
+ m_impl->m_wire.Flush();
+}
+
+void ClientImpl::SendValues(uint64_t curTimeMs) {
+ m_impl->SendValues(curTimeMs);
+ m_impl->m_wire.Flush();
+}
+
+void ClientImpl::SetLocal(LocalInterface* local) {
+ m_impl->m_local = local;
+}
+
+void ClientImpl::SendInitial() {
+ m_impl->SendInitialValues();
+ m_impl->m_wire.Flush();
+}
diff --git a/ntcore/src/main/native/cpp/net/ClientImpl.h b/ntcore/src/main/native/cpp/net/ClientImpl.h
new file mode 100644
index 0000000..0e7fd4a
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/ClientImpl.h
@@ -0,0 +1,57 @@
+// 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 <functional>
+#include <memory>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "NetworkInterface.h"
+#include "WireConnection.h"
+
+namespace wpi {
+class Logger;
+} // namespace wpi
+
+namespace nt {
+class PubSubOptionsImpl;
+class Value;
+} // namespace nt
+
+namespace nt::net {
+
+struct ClientMessage;
+class WireConnection;
+
+class ClientImpl {
+ public:
+ ClientImpl(
+ uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger,
+ std::function<void(int64_t serverTimeOffset, int64_t rtt2, bool valid)>
+ timeSyncUpdated,
+ std::function<void(uint32_t repeatMs)> setPeriodic);
+ ~ClientImpl();
+
+ void ProcessIncomingText(std::string_view data);
+ void ProcessIncomingBinary(std::span<const uint8_t> data);
+ void HandleLocal(std::vector<ClientMessage>&& msgs);
+
+ void SendControl(uint64_t curTimeMs);
+ void SendValues(uint64_t curTimeMs);
+
+ void SetLocal(LocalInterface* local);
+ void SendInitial();
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> m_impl;
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/Message.h b/ntcore/src/main/native/cpp/net/Message.h
new file mode 100644
index 0000000..a95c5e8
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/Message.h
@@ -0,0 +1,100 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include <wpi/json.h>
+
+#include "PubSubOptions.h"
+#include "networktables/NetworkTableValue.h"
+#include "ntcore_c.h"
+
+namespace nt::net {
+
+struct PublishMsg {
+ static constexpr std::string_view kMethodStr = "publish";
+ NT_Publisher pubHandle{0};
+ NT_Topic topicHandle{0}; // will be 0 when coming from network
+ std::string name;
+ std::string typeStr;
+ wpi::json properties;
+ PubSubOptionsImpl options; // will be empty when coming from network
+};
+
+struct UnpublishMsg {
+ static constexpr std::string_view kMethodStr = "unpublish";
+ NT_Publisher pubHandle{0};
+ NT_Topic topicHandle{0}; // will be 0 when coming from network
+};
+
+struct SetPropertiesMsg {
+ static constexpr std::string_view kMethodStr = "setproperties";
+ NT_Topic topicHandle{0}; // will be 0 when coming from network
+ std::string name;
+ wpi::json update;
+};
+
+struct SubscribeMsg {
+ static constexpr std::string_view kMethodStr = "subscribe";
+ NT_Subscriber subHandle{0};
+ std::vector<std::string> topicNames;
+ PubSubOptionsImpl options;
+};
+
+struct UnsubscribeMsg {
+ static constexpr std::string_view kMethodStr = "unsubscribe";
+ NT_Subscriber subHandle{0};
+};
+
+struct ClientValueMsg {
+ NT_Publisher pubHandle{0};
+ Value value;
+};
+
+struct ClientMessage {
+ using Contents =
+ std::variant<std::monostate, PublishMsg, UnpublishMsg, SetPropertiesMsg,
+ SubscribeMsg, UnsubscribeMsg, ClientValueMsg>;
+ Contents contents;
+};
+
+struct AnnounceMsg {
+ static constexpr std::string_view kMethodStr = "announce";
+ std::string name;
+ int64_t id{0};
+ std::string typeStr;
+ std::optional<int64_t> pubuid;
+ wpi::json properties;
+};
+
+struct UnannounceMsg {
+ static constexpr std::string_view kMethodStr = "unannounce";
+ std::string name;
+ int64_t id{0};
+};
+
+struct PropertiesUpdateMsg {
+ static constexpr std::string_view kMethodStr = "properties";
+ std::string name;
+ wpi::json update;
+ bool ack;
+};
+
+struct ServerValueMsg {
+ NT_Topic topic{0};
+ Value value;
+};
+
+struct ServerMessage {
+ using Contents = std::variant<std::monostate, AnnounceMsg, UnannounceMsg,
+ PropertiesUpdateMsg, ServerValueMsg>;
+ Contents contents;
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/NetworkInterface.h b/ntcore/src/main/native/cpp/net/NetworkInterface.h
new file mode 100644
index 0000000..3b2e7dd
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/NetworkInterface.h
@@ -0,0 +1,62 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <span>
+#include <string>
+#include <string_view>
+
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class json;
+} // namespace wpi
+
+namespace nt {
+class PubSubOptionsImpl;
+class Value;
+} // namespace nt
+
+namespace nt::net {
+
+class LocalInterface {
+ public:
+ virtual ~LocalInterface() = default;
+
+ virtual NT_Topic NetworkAnnounce(std::string_view name,
+ std::string_view typeStr,
+ const wpi::json& properties,
+ NT_Publisher pubHandle) = 0;
+ virtual void NetworkUnannounce(std::string_view name) = 0;
+ virtual void NetworkPropertiesUpdate(std::string_view name,
+ const wpi::json& update, bool ack) = 0;
+ virtual void NetworkSetValue(NT_Topic topicHandle, const Value& value) = 0;
+};
+
+class NetworkInterface {
+ public:
+ virtual ~NetworkInterface() = default;
+
+ virtual void Publish(NT_Publisher pubHandle, NT_Topic topicHandle,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptionsImpl& options) = 0;
+ virtual void Unpublish(NT_Publisher pubHandle, NT_Topic topicHandle) = 0;
+ virtual void SetProperties(NT_Topic topicHandle, std::string_view name,
+ const wpi::json& update) = 0;
+ virtual void Subscribe(NT_Subscriber subHandle,
+ std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options) = 0;
+ virtual void Unsubscribe(NT_Subscriber subHandle) = 0;
+ virtual void SetValue(NT_Publisher pubHandle, const Value& value) = 0;
+};
+
+class ILocalStorage : public LocalInterface {
+ public:
+ virtual void StartNetwork(NetworkInterface* network) = 0;
+ virtual void ClearNetwork() = 0;
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/NetworkLoopQueue.cpp b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.cpp
new file mode 100644
index 0000000..7c58c65
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.cpp
@@ -0,0 +1,54 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "NetworkLoopQueue.h"
+
+#include <wpi/Logger.h>
+
+using namespace nt::net;
+
+static constexpr size_t kMaxSize = 2 * 1024 * 1024;
+
+void NetworkLoopQueue::SetValue(NT_Publisher pubHandle, const Value& value) {
+ std::scoped_lock lock{m_mutex};
+ switch (value.type()) {
+ case NT_STRING:
+ m_size += value.GetString().size(); // imperfect but good enough
+ break;
+ case NT_RAW:
+ m_size += value.GetRaw().size_bytes();
+ break;
+ case NT_BOOLEAN_ARRAY:
+ m_size += value.GetBooleanArray().size_bytes();
+ break;
+ case NT_INTEGER_ARRAY:
+ m_size += value.GetIntegerArray().size_bytes();
+ break;
+ case NT_FLOAT_ARRAY:
+ m_size += value.GetFloatArray().size_bytes();
+ break;
+ case NT_DOUBLE_ARRAY:
+ m_size += value.GetDoubleArray().size_bytes();
+ break;
+ case NT_STRING_ARRAY: {
+ auto arr = value.GetStringArray();
+ m_size += arr.size_bytes();
+ for (auto&& s : arr) {
+ m_size += s.capacity();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ m_size += sizeof(ClientMessage);
+ if (m_size > kMaxSize) {
+ if (!m_sizeErrored) {
+ WPI_ERROR(m_logger, "NT: dropping value set due to memory limits");
+ m_sizeErrored = true;
+ }
+ return; // avoid potential out of memory
+ }
+ m_queue.emplace_back(ClientMessage{ClientValueMsg{pubHandle, value}});
+}
diff --git a/ntcore/src/main/native/cpp/net/NetworkLoopQueue.h b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.h
new file mode 100644
index 0000000..6ab68b6
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.h
@@ -0,0 +1,57 @@
+// 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 <span>
+#include <string>
+#include <vector>
+
+#include <wpi/mutex.h>
+
+#include "Message.h"
+#include "NetworkInterface.h"
+
+namespace wpi {
+class Logger;
+} // namespace wpi
+
+namespace nt::net {
+
+class NetworkLoopQueue : public NetworkInterface {
+ public:
+ static constexpr size_t kInitialQueueSize = 2000;
+
+ explicit NetworkLoopQueue(wpi::Logger& logger) : m_logger{logger} {
+ m_queue.reserve(kInitialQueueSize);
+ }
+
+ void ReadQueue(std::vector<ClientMessage>* out);
+ void ClearQueue();
+
+ // NetworkInterface - calls to these append to the queue
+ void Publish(NT_Publisher pubHandle, NT_Topic topicHandle,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptionsImpl& options) final;
+ void Unpublish(NT_Publisher pubHandle, NT_Topic topicHandle) final;
+ void SetProperties(NT_Topic topicHandle, std::string_view name,
+ const wpi::json& update) final;
+ void Subscribe(NT_Subscriber subHandle,
+ std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options) final;
+ void Unsubscribe(NT_Subscriber subHandle) final;
+ void SetValue(NT_Publisher pubHandle, const Value& value) final;
+
+ private:
+ wpi::mutex m_mutex;
+ std::vector<ClientMessage> m_queue;
+ wpi::Logger& m_logger;
+ size_t m_size{0};
+ bool m_sizeErrored{false};
+};
+
+} // namespace nt::net
+
+#include "NetworkLoopQueue.inc"
diff --git a/ntcore/src/main/native/cpp/net/NetworkLoopQueue.inc b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.inc
new file mode 100644
index 0000000..a441780
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.inc
@@ -0,0 +1,71 @@
+// 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 <span>
+#include <string>
+#include <vector>
+
+#include "NetworkLoopQueue.h"
+#include "ntcore_c.h"
+
+namespace nt::net {
+
+inline void NetworkLoopQueue::ReadQueue(std::vector<ClientMessage>* out) {
+ std::scoped_lock lock{m_mutex};
+ out->swap(m_queue);
+ m_queue.resize(0);
+ m_queue.reserve(out->capacity()); // keep the same running capacity
+ m_size = 0;
+ m_sizeErrored = false;
+}
+
+inline void NetworkLoopQueue::ClearQueue() {
+ std::scoped_lock lock{m_mutex};
+ m_queue.resize(0);
+ m_size = 0;
+ m_sizeErrored = false;
+}
+
+inline void NetworkLoopQueue::Publish(NT_Publisher pubHandle,
+ NT_Topic topicHandle,
+ std::string_view name,
+ std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptionsImpl& options) {
+ std::scoped_lock lock{m_mutex};
+ m_queue.emplace_back(
+ ClientMessage{PublishMsg{pubHandle, topicHandle, std::string{name},
+ std::string{typeStr}, properties, options}});
+}
+
+inline void NetworkLoopQueue::Unpublish(NT_Publisher pubHandle,
+ NT_Topic topicHandle) {
+ std::scoped_lock lock{m_mutex};
+ m_queue.emplace_back(ClientMessage{UnpublishMsg{pubHandle, topicHandle}});
+}
+
+inline void NetworkLoopQueue::SetProperties(NT_Topic topicHandle,
+ std::string_view name,
+ const wpi::json& update) {
+ std::scoped_lock lock{m_mutex};
+ m_queue.emplace_back(
+ ClientMessage{SetPropertiesMsg{topicHandle, std::string{name}, update}});
+}
+
+inline void NetworkLoopQueue::Subscribe(NT_Subscriber subHandle,
+ std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options) {
+ std::scoped_lock lock{m_mutex};
+ m_queue.emplace_back(ClientMessage{SubscribeMsg{
+ subHandle, {topicNames.begin(), topicNames.end()}, options}});
+}
+
+inline void NetworkLoopQueue::Unsubscribe(NT_Subscriber subHandle) {
+ std::scoped_lock lock{m_mutex};
+ m_queue.emplace_back(ClientMessage{UnsubscribeMsg{subHandle}});
+}
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/ServerImpl.cpp b/ntcore/src/main/native/cpp/net/ServerImpl.cpp
new file mode 100644
index 0000000..d9b1c0c
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/ServerImpl.cpp
@@ -0,0 +1,2363 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "ServerImpl.h"
+
+#include <stdint.h>
+
+#include <algorithm>
+#include <cmath>
+#include <numeric>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include <wpi/Base64.h>
+#include <wpi/DenseMap.h>
+#include <wpi/MessagePack.h>
+#include <wpi/SmallVector.h>
+#include <wpi/StringExtras.h>
+#include <wpi/StringMap.h>
+#include <wpi/UidVector.h>
+#include <wpi/json.h>
+#include <wpi/json_serializer.h>
+#include <wpi/raw_ostream.h>
+#include <wpi/timestamp.h>
+
+#include "IConnectionList.h"
+#include "Log.h"
+#include "Message.h"
+#include "NetworkInterface.h"
+#include "PubSubOptions.h"
+#include "Types_internal.h"
+#include "WireConnection.h"
+#include "WireDecoder.h"
+#include "WireEncoder.h"
+#include "net3/Message3.h"
+#include "net3/SequenceNumber.h"
+#include "net3/WireConnection3.h"
+#include "net3/WireDecoder3.h"
+#include "net3/WireEncoder3.h"
+#include "networktables/NetworkTableValue.h"
+#include "ntcore_c.h"
+
+using namespace nt;
+using namespace nt::net;
+using namespace mpack;
+
+static constexpr uint32_t kMinPeriodMs = 5;
+
+// maximum number of times the wire can be not ready to send another
+// transmission before we close the connection
+static constexpr int kWireMaxNotReady = 10;
+
+namespace {
+
+// Utility wrapper for making a set-like vector
+template <typename T>
+class VectorSet : public std::vector<T> {
+ public:
+ using iterator = typename std::vector<T>::iterator;
+ void Add(T value) { this->push_back(value); }
+ // returns true if element was present
+ bool Remove(T value) {
+ auto removeIt = std::remove(this->begin(), this->end(), value);
+ if (removeIt == this->end()) {
+ return false;
+ }
+ this->erase(removeIt, this->end());
+ return true;
+ }
+};
+
+struct PublisherData;
+struct SubscriberData;
+struct TopicData;
+class SImpl;
+
+class ClientData {
+ public:
+ ClientData(std::string_view originalName, std::string_view name,
+ std::string_view connInfo, bool local,
+ ServerImpl::SetPeriodicFunc setPeriodic, SImpl& server, int id,
+ wpi::Logger& logger)
+ : m_originalName{originalName},
+ m_name{name},
+ m_connInfo{connInfo},
+ m_local{local},
+ m_setPeriodic{std::move(setPeriodic)},
+ m_server{server},
+ m_id{id},
+ m_logger{logger} {}
+ virtual ~ClientData() = default;
+
+ virtual void ProcessIncomingText(std::string_view data) = 0;
+ virtual void ProcessIncomingBinary(std::span<const uint8_t> data) = 0;
+
+ enum SendMode { kSendDisabled = 0, kSendAll, kSendNormal, kSendImmNoFlush };
+
+ virtual void SendValue(TopicData* topic, const Value& value,
+ SendMode mode) = 0;
+ virtual void SendAnnounce(TopicData* topic,
+ std::optional<int64_t> pubuid) = 0;
+ virtual void SendUnannounce(TopicData* topic) = 0;
+ virtual void SendPropertiesUpdate(TopicData* topic, const wpi::json& update,
+ bool ack) = 0;
+ virtual void SendOutgoing(uint64_t curTimeMs) = 0;
+ virtual void Flush() = 0;
+
+ void UpdateMetaClientPub();
+ void UpdateMetaClientSub();
+
+ std::span<SubscriberData*> GetSubscribers(
+ std::string_view name, bool special,
+ wpi::SmallVectorImpl<SubscriberData*>& buf);
+
+ std::string_view GetOriginalName() const { return m_originalName; }
+ std::string_view GetName() const { return m_name; }
+ int GetId() const { return m_id; }
+
+ protected:
+ std::string m_originalName;
+ std::string m_name;
+ std::string m_connInfo;
+ bool m_local; // local to machine
+ ServerImpl::SetPeriodicFunc m_setPeriodic;
+ // TODO: make this per-topic?
+ uint32_t m_periodMs{UINT32_MAX};
+ uint64_t m_lastSendMs{0};
+ SImpl& m_server;
+ int m_id;
+
+ wpi::Logger& m_logger;
+
+ wpi::DenseMap<int64_t, std::unique_ptr<PublisherData>> m_publishers;
+ wpi::DenseMap<int64_t, std::unique_ptr<SubscriberData>> m_subscribers;
+
+ public:
+ // meta topics
+ TopicData* m_metaPub = nullptr;
+ TopicData* m_metaSub = nullptr;
+};
+
+class ClientData4Base : public ClientData, protected ClientMessageHandler {
+ public:
+ ClientData4Base(std::string_view originalName, std::string_view name,
+ std::string_view connInfo, bool local,
+ ServerImpl::SetPeriodicFunc setPeriodic, SImpl& server,
+ int id, wpi::Logger& logger)
+ : ClientData{originalName, name, connInfo, local,
+ setPeriodic, server, id, logger} {}
+
+ protected:
+ // ClientMessageHandler interface
+ void ClientPublish(int64_t pubuid, std::string_view name,
+ std::string_view typeStr,
+ const wpi::json& properties) final;
+ void ClientUnpublish(int64_t pubuid) final;
+ void ClientSetProperties(std::string_view name,
+ const wpi::json& update) final;
+ void ClientSubscribe(int64_t subuid, std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options) final;
+ void ClientUnsubscribe(int64_t subuid) final;
+
+ void ClientSetValue(int64_t pubuid, const Value& value);
+
+ wpi::DenseMap<TopicData*, bool> m_announceSent;
+};
+
+class ClientDataLocal final : public ClientData4Base {
+ public:
+ ClientDataLocal(SImpl& server, int id, wpi::Logger& logger)
+ : ClientData4Base{"", "", "", true, [](uint32_t) {}, server, id, logger} {
+ }
+
+ void ProcessIncomingText(std::string_view data) final {}
+ void ProcessIncomingBinary(std::span<const uint8_t> data) final {}
+
+ void SendValue(TopicData* topic, const Value& value, SendMode mode) final;
+ void SendAnnounce(TopicData* topic, std::optional<int64_t> pubuid) final;
+ void SendUnannounce(TopicData* topic) final;
+ void SendPropertiesUpdate(TopicData* topic, const wpi::json& update,
+ bool ack) final;
+ void SendOutgoing(uint64_t curTimeMs) final {}
+ void Flush() final {}
+
+ void HandleLocal(std::span<const ClientMessage> msgs);
+};
+
+class ClientData4 final : public ClientData4Base {
+ public:
+ ClientData4(std::string_view originalName, std::string_view name,
+ std::string_view connInfo, bool local, WireConnection& wire,
+ ServerImpl::SetPeriodicFunc setPeriodic, SImpl& server, int id,
+ wpi::Logger& logger)
+ : ClientData4Base{originalName, name, connInfo, local,
+ setPeriodic, server, id, logger},
+ m_wire{wire} {}
+
+ void ProcessIncomingText(std::string_view data) final;
+ void ProcessIncomingBinary(std::span<const uint8_t> data) final;
+
+ void SendValue(TopicData* topic, const Value& value, SendMode mode) final;
+ void SendAnnounce(TopicData* topic, std::optional<int64_t> pubuid) final;
+ void SendUnannounce(TopicData* topic) final;
+ void SendPropertiesUpdate(TopicData* topic, const wpi::json& update,
+ bool ack) final;
+ void SendOutgoing(uint64_t curTimeMs) final;
+
+ void Flush() final;
+
+ public:
+ WireConnection& m_wire;
+
+ private:
+ std::vector<ServerMessage> m_outgoing;
+ int m_notReadyCount{0};
+
+ bool WriteBinary(int64_t id, int64_t time, const Value& value) {
+ return WireEncodeBinary(SendBinary().Add(), id, time, value);
+ }
+
+ TextWriter& SendText() {
+ m_outBinary.reset(); // ensure proper interleaving of text and binary
+ if (!m_outText) {
+ m_outText = m_wire.SendText();
+ }
+ return *m_outText;
+ }
+
+ BinaryWriter& SendBinary() {
+ m_outText.reset(); // ensure proper interleaving of text and binary
+ if (!m_outBinary) {
+ m_outBinary = m_wire.SendBinary();
+ }
+ return *m_outBinary;
+ }
+
+ // valid when we are actively writing to this client
+ std::optional<TextWriter> m_outText;
+ std::optional<BinaryWriter> m_outBinary;
+};
+
+class ClientData3 final : public ClientData, private net3::MessageHandler3 {
+ public:
+ ClientData3(std::string_view connInfo, bool local,
+ net3::WireConnection3& wire, ServerImpl::Connected3Func connected,
+ ServerImpl::SetPeriodicFunc setPeriodic, SImpl& server, int id,
+ wpi::Logger& logger)
+ : ClientData{"", "", connInfo, local, setPeriodic, server, id, logger},
+ m_connected{std::move(connected)},
+ m_wire{wire},
+ m_decoder{*this} {}
+
+ void ProcessIncomingText(std::string_view data) final {}
+ void ProcessIncomingBinary(std::span<const uint8_t> data) final;
+
+ void SendValue(TopicData* topic, const Value& value, SendMode mode) final;
+ void SendAnnounce(TopicData* topic, std::optional<int64_t> pubuid) final;
+ void SendUnannounce(TopicData* topic) final;
+ void SendPropertiesUpdate(TopicData* topic, const wpi::json& update,
+ bool ack) final;
+ void SendOutgoing(uint64_t curTimeMs) final;
+
+ void Flush() final { m_wire.Flush(); }
+
+ private:
+ // MessageHandler3 interface
+ void KeepAlive() final;
+ void ServerHelloDone() final;
+ void ClientHelloDone() final;
+ void ClearEntries() final;
+ void ProtoUnsup(unsigned int proto_rev) final;
+ void ClientHello(std::string_view self_id, unsigned int proto_rev) final;
+ void ServerHello(unsigned int flags, std::string_view self_id) final;
+ void EntryAssign(std::string_view name, unsigned int id, unsigned int seq_num,
+ const Value& value, unsigned int flags) final;
+ void EntryUpdate(unsigned int id, unsigned int seq_num,
+ const Value& value) final;
+ void FlagsUpdate(unsigned int id, unsigned int flags) final;
+ void EntryDelete(unsigned int id) final;
+ void ExecuteRpc(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> params) final {}
+ void RpcResponse(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> result) final {}
+
+ ServerImpl::Connected3Func m_connected;
+ net3::WireConnection3& m_wire;
+
+ enum State { kStateInitial, kStateServerHelloComplete, kStateRunning };
+ State m_state{kStateInitial};
+ net3::WireDecoder3 m_decoder;
+
+ std::vector<net3::Message3> m_outgoing;
+ int64_t m_nextPubUid{1};
+ int m_notReadyCount{0};
+
+ struct TopicData3 {
+ explicit TopicData3(TopicData* topic) { UpdateFlags(topic); }
+
+ unsigned int flags{0};
+ net3::SequenceNumber seqNum;
+ bool sentAssign{false};
+ bool published{false};
+ int64_t pubuid{0};
+
+ bool UpdateFlags(TopicData* topic);
+ };
+ wpi::DenseMap<TopicData*, TopicData3> m_topics3;
+ TopicData3* GetTopic3(TopicData* topic) {
+ return &m_topics3.try_emplace(topic, topic).first->second;
+ }
+};
+
+struct TopicData {
+ TopicData(std::string_view name, std::string_view typeStr)
+ : name{name}, typeStr{typeStr} {}
+ TopicData(std::string_view name, std::string_view typeStr,
+ wpi::json properties)
+ : name{name}, typeStr{typeStr}, properties(std::move(properties)) {
+ RefreshProperties();
+ }
+
+ bool IsPublished() const {
+ return persistent || retained || !publishers.empty();
+ }
+
+ // returns true if properties changed
+ bool SetProperties(const wpi::json& update);
+ void RefreshProperties();
+ bool SetFlags(unsigned int flags_);
+
+ std::string name;
+ unsigned int id;
+ Value lastValue;
+ ClientData* lastValueClient = nullptr;
+ std::string typeStr;
+ wpi::json properties = wpi::json::object();
+ bool persistent{false};
+ bool retained{false};
+ bool special{false};
+ NT_Topic localHandle{0};
+
+ VectorSet<PublisherData*> publishers;
+ VectorSet<SubscriberData*> subscribers;
+
+ // meta topics
+ TopicData* metaPub = nullptr;
+ TopicData* metaSub = nullptr;
+};
+
+struct PublisherData {
+ PublisherData(ClientData* client, TopicData* topic, int64_t pubuid)
+ : client{client}, topic{topic}, pubuid{pubuid} {}
+
+ ClientData* client;
+ TopicData* topic;
+ int64_t pubuid;
+};
+
+struct SubscriberData {
+ SubscriberData(ClientData* client, std::span<const std::string> topicNames,
+ int64_t subuid, const PubSubOptionsImpl& options)
+ : client{client},
+ topicNames{topicNames.begin(), topicNames.end()},
+ subuid{subuid},
+ options{options},
+ periodMs(std::lround(options.periodicMs / 10.0) * 10) {
+ if (periodMs < kMinPeriodMs) {
+ periodMs = kMinPeriodMs;
+ }
+ }
+
+ void Update(std::span<const std::string> topicNames_,
+ const PubSubOptionsImpl& options_) {
+ topicNames = {topicNames_.begin(), topicNames_.end()};
+ options = options_;
+ periodMs = std::lround(options_.periodicMs / 10.0) * 10;
+ if (periodMs < kMinPeriodMs) {
+ periodMs = kMinPeriodMs;
+ }
+ }
+
+ bool Matches(std::string_view name, bool special);
+
+ ClientData* client;
+ std::vector<std::string> topicNames;
+ int64_t subuid;
+ PubSubOptionsImpl options;
+ // in options as double, but copy here as integer; rounded to the nearest
+ // 10 ms
+ uint32_t periodMs;
+};
+
+class SImpl {
+ public:
+ explicit SImpl(wpi::Logger& logger);
+
+ wpi::Logger& m_logger;
+ LocalInterface* m_local{nullptr};
+ bool m_controlReady{false};
+
+ ClientDataLocal* m_localClient;
+ std::vector<std::unique_ptr<ClientData>> m_clients;
+ wpi::UidVector<std::unique_ptr<TopicData>, 16> m_topics;
+ wpi::StringMap<TopicData*> m_nameTopics;
+ bool m_persistentChanged{false};
+
+ // global meta topics (other meta topics are linked to from the specific
+ // client or topic)
+ TopicData* m_metaClients;
+
+ // ServerImpl interface
+ std::pair<std::string, int> AddClient(
+ std::string_view name, std::string_view connInfo, bool local,
+ WireConnection& wire, ServerImpl::SetPeriodicFunc setPeriodic);
+ int AddClient3(std::string_view connInfo, bool local,
+ net3::WireConnection3& wire,
+ ServerImpl::Connected3Func connected,
+ ServerImpl::SetPeriodicFunc setPeriodic);
+ void RemoveClient(int clientId);
+
+ bool PersistentChanged();
+ void DumpPersistent(wpi::raw_ostream& os);
+ std::string LoadPersistent(std::string_view in);
+
+ // helper functions
+ TopicData* CreateTopic(ClientData* client, std::string_view name,
+ std::string_view typeStr, const wpi::json& properties,
+ bool special = false);
+ TopicData* CreateMetaTopic(std::string_view name);
+ void DeleteTopic(TopicData* topic);
+ void SetProperties(ClientData* client, TopicData* topic,
+ const wpi::json& update);
+ void SetFlags(ClientData* client, TopicData* topic, unsigned int flags);
+ void SetValue(ClientData* client, TopicData* topic, const Value& value);
+
+ // update meta topic values from data structures
+ void UpdateMetaClients(const std::vector<ConnectionInfo>& conns);
+ void UpdateMetaTopicPub(TopicData* topic);
+ void UpdateMetaTopicSub(TopicData* topic);
+
+ private:
+ void PropertiesChanged(ClientData* client, TopicData* topic,
+ const wpi::json& update);
+};
+
+struct Writer : public mpack_writer_t {
+ Writer() {
+ mpack_writer_init(this, buf, sizeof(buf));
+ mpack_writer_set_context(this, &os);
+ mpack_writer_set_flush(
+ this, [](mpack_writer_t* w, const char* buffer, size_t count) {
+ static_cast<wpi::raw_ostream*>(w->context)->write(buffer, count);
+ });
+ }
+
+ std::vector<uint8_t> bytes;
+ wpi::raw_uvector_ostream os{bytes};
+ char buf[128];
+};
+} // namespace
+
+static void WriteOptions(mpack_writer_t& w, const PubSubOptionsImpl& options) {
+ int size =
+ (options.sendAll ? 1 : 0) + (options.topicsOnly ? 1 : 0) +
+ (options.periodicMs != PubSubOptionsImpl::kDefaultPeriodicMs ? 1 : 0) +
+ (options.prefixMatch ? 1 : 0);
+ mpack_start_map(&w, size);
+ if (options.sendAll) {
+ mpack_write_str(&w, "all");
+ mpack_write_bool(&w, true);
+ }
+ if (options.topicsOnly) {
+ mpack_write_str(&w, "topicsonly");
+ mpack_write_bool(&w, true);
+ }
+ if (options.periodicMs != PubSubOptionsImpl::kDefaultPeriodicMs) {
+ mpack_write_str(&w, "periodic");
+ mpack_write_float(&w, options.periodicMs / 1000.0);
+ }
+ if (options.prefixMatch) {
+ mpack_write_str(&w, "prefix");
+ mpack_write_bool(&w, true);
+ }
+ mpack_finish_map(&w);
+}
+
+void ClientData::UpdateMetaClientPub() {
+ if (!m_metaPub) {
+ return;
+ }
+ Writer w;
+ mpack_start_array(&w, m_publishers.size());
+ for (auto&& pub : m_publishers) {
+ mpack_start_map(&w, 2);
+ mpack_write_str(&w, "uid");
+ mpack_write_int(&w, pub.first);
+ mpack_write_str(&w, "topic");
+ mpack_write_str(&w, pub.second->topic->name);
+ mpack_finish_map(&w);
+ }
+ mpack_finish_array(&w);
+ if (mpack_writer_destroy(&w) == mpack_ok) {
+ m_server.SetValue(nullptr, m_metaPub, Value::MakeRaw(std::move(w.bytes)));
+ }
+}
+
+void ClientData::UpdateMetaClientSub() {
+ if (!m_metaSub) {
+ return;
+ }
+ Writer w;
+ mpack_start_array(&w, m_subscribers.size());
+ for (auto&& sub : m_subscribers) {
+ mpack_start_map(&w, 3);
+ mpack_write_str(&w, "uid");
+ mpack_write_int(&w, sub.first);
+ mpack_write_str(&w, "topics");
+ mpack_start_array(&w, sub.second->topicNames.size());
+ for (auto&& name : sub.second->topicNames) {
+ mpack_write_str(&w, name);
+ }
+ mpack_finish_array(&w);
+ mpack_write_str(&w, "options");
+ WriteOptions(w, sub.second->options);
+ mpack_finish_map(&w);
+ }
+ mpack_finish_array(&w);
+ if (mpack_writer_destroy(&w) == mpack_ok) {
+ m_server.SetValue(nullptr, m_metaSub, Value::MakeRaw(std::move(w.bytes)));
+ }
+}
+
+std::span<SubscriberData*> ClientData::GetSubscribers(
+ std::string_view name, bool special,
+ wpi::SmallVectorImpl<SubscriberData*>& buf) {
+ buf.resize(0);
+ for (auto&& subPair : m_subscribers) {
+ SubscriberData* subscriber = subPair.getSecond().get();
+ if (subscriber->Matches(name, special)) {
+ buf.emplace_back(subscriber);
+ }
+ }
+ return {buf.data(), buf.size()};
+}
+
+void ClientData4Base::ClientPublish(int64_t pubuid, std::string_view name,
+ std::string_view typeStr,
+ const wpi::json& properties) {
+ DEBUG3("ClientPublish({}, {}, {}, {})", m_id, name, pubuid, typeStr);
+ auto topic = m_server.CreateTopic(this, name, typeStr, properties);
+
+ // create publisher
+ auto [publisherIt, isNew] = m_publishers.try_emplace(
+ pubuid, std::make_unique<PublisherData>(this, topic, pubuid));
+ if (!isNew) {
+ WARNING("client {} duplicate publish of pubuid {}", m_id, pubuid);
+ }
+
+ // add publisher to topic
+ topic->publishers.Add(publisherIt->getSecond().get());
+
+ // update meta data
+ m_server.UpdateMetaTopicPub(topic);
+ UpdateMetaClientPub();
+
+ // respond with announce with pubuid to client
+ DEBUG4("client {}: announce {} pubuid {}", m_id, topic->name, pubuid);
+ SendAnnounce(topic, pubuid);
+}
+
+void ClientData4Base::ClientUnpublish(int64_t pubuid) {
+ DEBUG3("ClientUnpublish({}, {})", m_id, pubuid);
+ auto publisherIt = m_publishers.find(pubuid);
+ if (publisherIt == m_publishers.end()) {
+ return; // nothing to do
+ }
+ auto publisher = publisherIt->getSecond().get();
+ auto topic = publisher->topic;
+
+ // remove publisher from topic
+ topic->publishers.Remove(publisher);
+
+ // remove publisher from client
+ m_publishers.erase(publisherIt);
+
+ // update meta data
+ m_server.UpdateMetaTopicPub(topic);
+ UpdateMetaClientPub();
+
+ // delete topic if no longer published
+ if (!topic->IsPublished()) {
+ m_server.DeleteTopic(topic);
+ }
+}
+
+void ClientData4Base::ClientSetProperties(std::string_view name,
+ const wpi::json& update) {
+ DEBUG4("ClientSetProperties({}, {}, {})", m_id, name, update.dump());
+ auto topicIt = m_server.m_nameTopics.find(name);
+ if (topicIt == m_server.m_nameTopics.end() ||
+ !topicIt->second->IsPublished()) {
+ DEBUG3("ignored SetProperties from {} on non-existent topic '{}'", m_id,
+ name);
+ return; // nothing to do
+ }
+ auto topic = topicIt->second;
+ if (topic->special) {
+ DEBUG3("ignored SetProperties from {} on meta topic '{}'", m_id, name);
+ return; // nothing to do
+ }
+ m_server.SetProperties(nullptr, topic, update);
+}
+
+void ClientData4Base::ClientSubscribe(int64_t subuid,
+ std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options) {
+ DEBUG4("ClientSubscribe({}, ({}), {})", m_id, fmt::join(topicNames, ","),
+ subuid);
+ auto& sub = m_subscribers[subuid];
+ bool replace = false;
+ if (sub) {
+ // replace subscription
+ sub->Update(topicNames, options);
+ replace = true;
+ } else {
+ // create
+ sub = std::make_unique<SubscriberData>(this, topicNames, subuid, options);
+ }
+
+ // limit subscriber min period
+ if (sub->periodMs < kMinPeriodMs) {
+ sub->periodMs = kMinPeriodMs;
+ }
+
+ // update periodic sender (if not local)
+ if (!m_local) {
+ if (m_periodMs == UINT32_MAX) {
+ m_periodMs = sub->periodMs;
+ } else {
+ m_periodMs = std::gcd(m_periodMs, sub->periodMs);
+ }
+ if (m_periodMs < kMinPeriodMs) {
+ m_periodMs = kMinPeriodMs;
+ }
+ m_setPeriodic(m_periodMs);
+ }
+
+ // see if this immediately subscribes to any topics
+ for (auto&& topic : m_server.m_topics) {
+ bool removed = false;
+ if (replace) {
+ removed = topic->subscribers.Remove(sub.get());
+ }
+
+ // is client already subscribed?
+ bool wasSubscribed = false;
+ for (auto subscriber : topic->subscribers) {
+ if (subscriber->client == this) {
+ wasSubscribed = true;
+ break;
+ }
+ }
+
+ bool added = false;
+ if (sub->Matches(topic->name, topic->special)) {
+ topic->subscribers.Add(sub.get());
+ added = true;
+ }
+
+ if (added ^ removed) {
+ m_server.UpdateMetaTopicSub(topic.get());
+ }
+
+ if (!wasSubscribed && added && !removed) {
+ // announce topic to client
+ DEBUG4("client {}: announce {}", m_id, topic->name);
+ SendAnnounce(topic.get(), std::nullopt);
+
+ // send last value
+ if (!sub->options.topicsOnly && topic->lastValue) {
+ DEBUG4("send last value for {} to client {}", topic->name, m_id);
+ SendValue(topic.get(), topic->lastValue, kSendAll);
+ }
+ }
+ }
+
+ // update meta data
+ UpdateMetaClientSub();
+
+ Flush();
+}
+
+void ClientData4Base::ClientUnsubscribe(int64_t subuid) {
+ DEBUG3("ClientUnsubscribe({}, {})", m_id, subuid);
+ auto subIt = m_subscribers.find(subuid);
+ if (subIt == m_subscribers.end() || !subIt->getSecond()) {
+ return; // nothing to do
+ }
+ auto sub = subIt->getSecond().get();
+
+ // remove from topics
+ for (auto&& topic : m_server.m_topics) {
+ if (topic->subscribers.Remove(sub)) {
+ m_server.UpdateMetaTopicSub(topic.get());
+ }
+ }
+
+ // delete it from client (future value sets will be ignored)
+ m_subscribers.erase(subIt);
+ UpdateMetaClientSub();
+
+ // loop over all publishers to update period
+ m_periodMs = UINT32_MAX;
+ for (auto&& sub : m_subscribers) {
+ if (m_periodMs == UINT32_MAX) {
+ m_periodMs = sub.getSecond()->periodMs;
+ } else {
+ m_periodMs = std::gcd(m_periodMs, sub.getSecond()->periodMs);
+ }
+ }
+ if (m_periodMs < kMinPeriodMs) {
+ m_periodMs = kMinPeriodMs;
+ }
+ m_setPeriodic(m_periodMs);
+}
+
+void ClientData4Base::ClientSetValue(int64_t pubuid, const Value& value) {
+ DEBUG4("ClientSetValue({}, {})", m_id, pubuid);
+ auto publisherIt = m_publishers.find(pubuid);
+ if (publisherIt == m_publishers.end()) {
+ WARNING("unrecognized client {} pubuid {}, ignoring set", m_id, pubuid);
+ return; // ignore unrecognized pubuids
+ }
+ auto topic = publisherIt->getSecond().get()->topic;
+ m_server.SetValue(this, topic, value);
+}
+
+void ClientDataLocal::SendValue(TopicData* topic, const Value& value,
+ SendMode mode) {
+ if (m_server.m_local) {
+ m_server.m_local->NetworkSetValue(topic->localHandle, value);
+ }
+}
+
+void ClientDataLocal::SendAnnounce(TopicData* topic,
+ std::optional<int64_t> pubuid) {
+ if (m_server.m_local) {
+ auto& sent = m_announceSent[topic];
+ if (sent) {
+ return;
+ }
+ sent = true;
+
+ topic->localHandle = m_server.m_local->NetworkAnnounce(
+ topic->name, topic->typeStr, topic->properties, pubuid.value_or(0));
+ }
+}
+
+void ClientDataLocal::SendUnannounce(TopicData* topic) {
+ if (m_server.m_local) {
+ auto& sent = m_announceSent[topic];
+ if (!sent) {
+ return;
+ }
+ sent = false;
+ m_server.m_local->NetworkUnannounce(topic->name);
+ }
+}
+
+void ClientDataLocal::SendPropertiesUpdate(TopicData* topic,
+ const wpi::json& update, bool ack) {
+ if (m_server.m_local) {
+ if (!m_announceSent.lookup(topic)) {
+ return;
+ }
+ m_server.m_local->NetworkPropertiesUpdate(topic->name, update, ack);
+ }
+}
+
+void ClientDataLocal::HandleLocal(std::span<const ClientMessage> msgs) {
+ DEBUG4("HandleLocal()");
+ // just map as a normal client into client=0 calls
+ for (const auto& elem : msgs) { // NOLINT
+ // common case is value, so check that first
+ if (auto msg = std::get_if<ClientValueMsg>(&elem.contents)) {
+ ClientSetValue(msg->pubHandle, msg->value);
+ } else if (auto msg = std::get_if<PublishMsg>(&elem.contents)) {
+ ClientPublish(msg->pubHandle, msg->name, msg->typeStr, msg->properties);
+ } else if (auto msg = std::get_if<UnpublishMsg>(&elem.contents)) {
+ ClientUnpublish(msg->pubHandle);
+ } else if (auto msg = std::get_if<SetPropertiesMsg>(&elem.contents)) {
+ ClientSetProperties(msg->name, msg->update);
+ } else if (auto msg = std::get_if<SubscribeMsg>(&elem.contents)) {
+ ClientSubscribe(msg->subHandle, msg->topicNames, msg->options);
+ } else if (auto msg = std::get_if<UnsubscribeMsg>(&elem.contents)) {
+ ClientUnsubscribe(msg->subHandle);
+ }
+ }
+}
+
+void ClientData4::ProcessIncomingText(std::string_view data) {
+ WireDecodeText(data, *this, m_logger);
+}
+
+void ClientData4::ProcessIncomingBinary(std::span<const uint8_t> data) {
+ for (;;) {
+ if (data.empty()) {
+ break;
+ }
+
+ // decode message
+ int64_t pubuid;
+ Value value;
+ std::string error;
+ if (!WireDecodeBinary(&data, &pubuid, &value, &error, 0)) {
+ m_wire.Disconnect(fmt::format("binary decode error: {}", error));
+ break;
+ }
+
+ // respond to RTT ping
+ if (pubuid == -1) {
+ auto now = wpi::Now();
+ DEBUG4("RTT ping from {}, responding with time={}", m_id, now);
+ {
+ auto out = m_wire.SendBinary();
+ WireEncodeBinary(out.Add(), -1, now, value);
+ }
+ m_wire.Flush();
+ continue;
+ }
+
+ // handle value set
+ ClientSetValue(pubuid, value);
+ }
+}
+
+void ClientData4::SendValue(TopicData* topic, const Value& value,
+ SendMode mode) {
+ if (m_local) {
+ mode = ClientData::kSendImmNoFlush; // always send local immediately
+ }
+ switch (mode) {
+ case ClientData::kSendDisabled: // do nothing
+ break;
+ case ClientData::kSendImmNoFlush: // send immediately
+ WriteBinary(topic->id, value.time(), value);
+ if (m_local) {
+ Flush();
+ }
+ break;
+ case ClientData::kSendAll: // append to outgoing
+ m_outgoing.emplace_back(ServerMessage{ServerValueMsg{topic->id, value}});
+ break;
+ case ClientData::kSendNormal: {
+ // scan outgoing and replace, or append if not present
+ bool found = false;
+ for (auto&& msg : m_outgoing) {
+ if (auto m = std::get_if<ServerValueMsg>(&msg.contents)) {
+ if (m->topic == topic->id) {
+ m->value = value;
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ m_outgoing.emplace_back(
+ ServerMessage{ServerValueMsg{topic->id, value}});
+ }
+ break;
+ }
+ }
+}
+
+void ClientData4::SendAnnounce(TopicData* topic,
+ std::optional<int64_t> pubuid) {
+ auto& sent = m_announceSent[topic];
+ if (sent) {
+ return;
+ }
+ sent = true;
+
+ if (m_local) {
+ WireEncodeAnnounce(SendText().Add(), topic->name, topic->id, topic->typeStr,
+ topic->properties, pubuid);
+ Flush();
+ } else {
+ m_outgoing.emplace_back(ServerMessage{AnnounceMsg{
+ topic->name, topic->id, topic->typeStr, pubuid, topic->properties}});
+ m_server.m_controlReady = true;
+ }
+}
+
+void ClientData4::SendUnannounce(TopicData* topic) {
+ auto& sent = m_announceSent[topic];
+ if (!sent) {
+ return;
+ }
+ sent = false;
+
+ if (m_local) {
+ WireEncodeUnannounce(SendText().Add(), topic->name, topic->id);
+ Flush();
+ } else {
+ m_outgoing.emplace_back(
+ ServerMessage{UnannounceMsg{topic->name, topic->id}});
+ m_server.m_controlReady = true;
+ }
+}
+
+void ClientData4::SendPropertiesUpdate(TopicData* topic,
+ const wpi::json& update, bool ack) {
+ if (!m_announceSent.lookup(topic)) {
+ return;
+ }
+
+ if (m_local) {
+ WireEncodePropertiesUpdate(SendText().Add(), topic->name, update, ack);
+ Flush();
+ } else {
+ m_outgoing.emplace_back(
+ ServerMessage{PropertiesUpdateMsg{topic->name, update, ack}});
+ m_server.m_controlReady = true;
+ }
+}
+
+void ClientData4::SendOutgoing(uint64_t curTimeMs) {
+ if (m_outgoing.empty()) {
+ return; // nothing to do
+ }
+
+ // rate limit frequency of transmissions
+ if (curTimeMs < (m_lastSendMs + kMinPeriodMs)) {
+ return;
+ }
+
+ if (!m_wire.Ready()) {
+ ++m_notReadyCount;
+ if (m_notReadyCount > kWireMaxNotReady) {
+ m_wire.Disconnect("transmit stalled");
+ }
+ return;
+ }
+ m_notReadyCount = 0;
+
+ for (auto&& msg : m_outgoing) {
+ if (auto m = std::get_if<ServerValueMsg>(&msg.contents)) {
+ WriteBinary(m->topic, m->value.time(), m->value);
+ } else {
+ WireEncodeText(SendText().Add(), msg);
+ }
+ }
+ m_outgoing.resize(0);
+ m_lastSendMs = curTimeMs;
+}
+
+void ClientData4::Flush() {
+ m_outText.reset();
+ m_outBinary.reset();
+ m_wire.Flush();
+}
+
+bool ClientData3::TopicData3::UpdateFlags(TopicData* topic) {
+ unsigned int newFlags = topic->persistent ? NT_PERSISTENT : 0;
+ bool updated = flags != newFlags;
+ flags = newFlags;
+ return updated;
+}
+
+void ClientData3::ProcessIncomingBinary(std::span<const uint8_t> data) {
+ if (!m_decoder.Execute(&data)) {
+ m_wire.Disconnect(m_decoder.GetError());
+ }
+}
+
+void ClientData3::SendValue(TopicData* topic, const Value& value,
+ SendMode mode) {
+ if (m_state != kStateRunning) {
+ if (mode == kSendImmNoFlush) {
+ mode = kSendAll;
+ }
+ } else if (m_local) {
+ mode = ClientData::kSendImmNoFlush; // always send local immediately
+ }
+ TopicData3* topic3 = GetTopic3(topic);
+
+ switch (mode) {
+ case ClientData::kSendDisabled: // do nothing
+ break;
+ case ClientData::kSendImmNoFlush: // send immediately and flush
+ ++topic3->seqNum;
+ if (topic3->sentAssign) {
+ net3::WireEncodeEntryUpdate(m_wire.Send().stream(), topic->id,
+ topic3->seqNum.value(), value);
+ } else {
+ net3::WireEncodeEntryAssign(m_wire.Send().stream(), topic->name,
+ topic->id, topic3->seqNum.value(), value,
+ topic3->flags);
+ topic3->sentAssign = true;
+ }
+ if (m_local) {
+ Flush();
+ }
+ break;
+ case ClientData::kSendNormal: {
+ // scan outgoing and replace, or append if not present
+ bool found = false;
+ for (auto&& msg : m_outgoing) {
+ if (msg.Is(net3::Message3::kEntryUpdate) ||
+ msg.Is(net3::Message3::kEntryAssign)) {
+ if (msg.id() == topic->id) {
+ msg.SetValue(value);
+ found = true;
+ break;
+ }
+ }
+ }
+ if (found) {
+ break;
+ }
+ }
+ // fallthrough
+ case ClientData::kSendAll: // append to outgoing
+ ++topic3->seqNum;
+ if (topic3->sentAssign) {
+ m_outgoing.emplace_back(net3::Message3::EntryUpdate(
+ topic->id, topic3->seqNum.value(), value));
+ } else {
+ m_outgoing.emplace_back(net3::Message3::EntryAssign(
+ topic->name, topic->id, topic3->seqNum.value(), value,
+ topic3->flags));
+ topic3->sentAssign = true;
+ }
+ break;
+ }
+}
+
+void ClientData3::SendAnnounce(TopicData* topic,
+ std::optional<int64_t> pubuid) {
+ // ignore if we've not yet built the subscriber
+ if (m_subscribers.empty()) {
+ return;
+ }
+
+ // subscribe to all non-special topics
+ if (!topic->special) {
+ topic->subscribers.Add(m_subscribers[0].get());
+ m_server.UpdateMetaTopicSub(topic);
+ }
+
+ // NT3 requires a value to send the assign message, so the assign message
+ // will get sent when the first value is sent (by SendValue).
+}
+
+void ClientData3::SendUnannounce(TopicData* topic) {
+ auto it = m_topics3.find(topic);
+ if (it == m_topics3.end()) {
+ return; // never sent to client
+ }
+ bool sentAssign = it->second.sentAssign;
+ m_topics3.erase(it);
+ if (!sentAssign) {
+ return; // never sent to client
+ }
+
+ // map to NT3 delete message
+ if (m_local && m_state == kStateRunning) {
+ net3::WireEncodeEntryDelete(m_wire.Send().stream(), topic->id);
+ Flush();
+ } else {
+ m_outgoing.emplace_back(net3::Message3::EntryDelete(topic->id));
+ }
+}
+
+void ClientData3::SendPropertiesUpdate(TopicData* topic,
+ const wpi::json& update, bool ack) {
+ if (ack) {
+ return; // we don't ack in NT3
+ }
+ auto it = m_topics3.find(topic);
+ if (it == m_topics3.end()) {
+ return; // never sent to client
+ }
+ TopicData3* topic3 = &it->second;
+ // Don't send flags update unless we've already sent an assign message.
+ // The assign message will contain the updated flags when we eventually
+ // send it.
+ if (topic3->UpdateFlags(topic) && topic3->sentAssign) {
+ if (m_local && m_state == kStateRunning) {
+ net3::WireEncodeFlagsUpdate(m_wire.Send().stream(), topic->id,
+ topic3->flags);
+ Flush();
+ } else {
+ m_outgoing.emplace_back(
+ net3::Message3::FlagsUpdate(topic->id, topic3->flags));
+ }
+ }
+}
+
+void ClientData3::SendOutgoing(uint64_t curTimeMs) {
+ if (m_outgoing.empty() || m_state != kStateRunning) {
+ return; // nothing to do
+ }
+
+ // rate limit frequency of transmissions
+ if (curTimeMs < (m_lastSendMs + kMinPeriodMs)) {
+ return;
+ }
+
+ if (!m_wire.Ready()) {
+ ++m_notReadyCount;
+ if (m_notReadyCount > kWireMaxNotReady) {
+ m_wire.Disconnect("transmit stalled");
+ }
+ return;
+ }
+ m_notReadyCount = 0;
+
+ auto out = m_wire.Send();
+ for (auto&& msg : m_outgoing) {
+ net3::WireEncode(out.stream(), msg);
+ }
+ m_outgoing.resize(0);
+ m_lastSendMs = curTimeMs;
+}
+
+void ClientData3::KeepAlive() {
+ DEBUG4("KeepAlive({})", m_id);
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received unexpected KeepAlive message");
+ return;
+ }
+ // ignore
+}
+
+void ClientData3::ServerHelloDone() {
+ DEBUG4("ServerHelloDone({})", m_id);
+ m_decoder.SetError("received unexpected ServerHelloDone message");
+}
+
+void ClientData3::ClientHelloDone() {
+ DEBUG4("ClientHelloDone({})", m_id);
+ if (m_state != kStateServerHelloComplete) {
+ m_decoder.SetError("received unexpected ClientHelloDone message");
+ return;
+ }
+ m_state = kStateRunning;
+}
+
+void ClientData3::ClearEntries() {
+ DEBUG4("ClearEntries({})", m_id);
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received unexpected ClearEntries message");
+ return;
+ }
+
+ for (auto topic3it : m_topics3) {
+ TopicData* topic = topic3it.first;
+
+ // make sure we send assign the next time
+ topic3it.second.sentAssign = false;
+
+ // unpublish from this client (if it was previously published)
+ if (topic3it.second.published) {
+ topic3it.second.published = false;
+ auto publisherIt = m_publishers.find(topic3it.second.pubuid);
+ if (publisherIt != m_publishers.end()) {
+ // remove publisher from topic
+ topic->publishers.Remove(publisherIt->second.get());
+
+ // remove publisher from client
+ m_publishers.erase(publisherIt);
+
+ // update meta data
+ m_server.UpdateMetaTopicPub(topic);
+ UpdateMetaClientPub();
+ }
+ }
+
+ // set retained=false
+ m_server.SetProperties(this, topic, {{"retained", false}});
+ }
+}
+
+void ClientData3::ProtoUnsup(unsigned int proto_rev) {
+ DEBUG4("ProtoUnsup({})", m_id);
+ m_decoder.SetError("received unexpected ProtoUnsup message");
+}
+
+void ClientData3::ClientHello(std::string_view self_id,
+ unsigned int proto_rev) {
+ DEBUG4("ClientHello({}, '{}', {:04x})", m_id, self_id, proto_rev);
+ if (m_state != kStateInitial) {
+ m_decoder.SetError("received unexpected ClientHello message");
+ return;
+ }
+ if (proto_rev != 0x0300) {
+ net3::WireEncodeProtoUnsup(m_wire.Send().stream(), 0x0300);
+ Flush();
+ m_decoder.SetError(
+ fmt::format("unsupported protocol version {:04x}", proto_rev));
+ return;
+ }
+ // create a unique name (just ignore provided client id)
+ m_name = fmt::format("NT3@{}", m_connInfo);
+ m_connected(m_name, 0x0300);
+ m_connected = nullptr; // no longer required
+
+ // create client meta topics
+ m_metaPub = m_server.CreateMetaTopic(fmt::format("$clientpub${}", m_name));
+ m_metaSub = m_server.CreateMetaTopic(fmt::format("$clientsub${}", m_name));
+
+ // subscribe and send initial assignments
+ auto& sub = m_subscribers[0];
+ std::string prefix;
+ PubSubOptions options;
+ options.prefixMatch = true;
+ sub = std::make_unique<SubscriberData>(
+ this, std::span<const std::string>{{prefix}}, 0, options);
+ m_periodMs = std::gcd(m_periodMs, sub->periodMs);
+ if (m_periodMs < kMinPeriodMs) {
+ m_periodMs = kMinPeriodMs;
+ }
+ m_setPeriodic(m_periodMs);
+
+ {
+ auto out = m_wire.Send();
+ net3::WireEncodeServerHello(out.stream(), 0, "server");
+ for (auto&& topic : m_server.m_topics) {
+ if (topic && !topic->special && topic->IsPublished() &&
+ topic->lastValue) {
+ DEBUG4("client {}: initial announce of '{}' (id {})", m_id, topic->name,
+ topic->id);
+ topic->subscribers.Add(sub.get());
+ m_server.UpdateMetaTopicSub(topic.get());
+
+ TopicData3* topic3 = GetTopic3(topic.get());
+ ++topic3->seqNum;
+ net3::WireEncodeEntryAssign(out.stream(), topic->name, topic->id,
+ topic3->seqNum.value(), topic->lastValue,
+ topic3->flags);
+ topic3->sentAssign = true;
+ }
+ }
+ net3::WireEncodeServerHelloDone(out.stream());
+ }
+ Flush();
+ m_state = kStateServerHelloComplete;
+
+ // update meta topics
+ UpdateMetaClientPub();
+ UpdateMetaClientSub();
+}
+
+void ClientData3::ServerHello(unsigned int flags, std::string_view self_id) {
+ DEBUG4("ServerHello({}, {}, {})", m_id, flags, self_id);
+ m_decoder.SetError("received unexpected ServerHello message");
+}
+
+void ClientData3::EntryAssign(std::string_view name, unsigned int id,
+ unsigned int seq_num, const Value& value,
+ unsigned int flags) {
+ DEBUG4("EntryAssign({}, {}, {}, {}, {})", m_id, id, seq_num,
+ static_cast<int>(value.type()), flags);
+ if (id != 0xffff) {
+ DEBUG3("ignored EntryAssign from {} with non-0xffff id {}", m_id, id);
+ return;
+ }
+
+ // convert from NT3 info
+ auto typeStr = TypeToString(value.type());
+ wpi::json properties = wpi::json::object();
+ properties["retained"] = true; // treat all NT3 published topics as retained
+ if ((flags & NT_PERSISTENT) != 0) {
+ properties["persistent"] = true;
+ }
+
+ // create topic
+ auto topic = m_server.CreateTopic(this, name, typeStr, properties);
+ TopicData3* topic3 = GetTopic3(topic);
+ if (topic3->published || topic3->sentAssign) {
+ WARNING("ignorning client {} duplicate publish of '{}'", m_id, name);
+ return;
+ }
+ ++topic3->seqNum;
+ topic3->published = true;
+ topic3->pubuid = m_nextPubUid++;
+ topic3->sentAssign = true;
+
+ // create publisher
+ auto [publisherIt, isNew] = m_publishers.try_emplace(
+ topic3->pubuid,
+ std::make_unique<PublisherData>(this, topic, topic3->pubuid));
+ if (!isNew) {
+ return; // shouldn't happen, but just in case...
+ }
+
+ // add publisher to topic
+ topic->publishers.Add(publisherIt->getSecond().get());
+
+ // update meta data
+ m_server.UpdateMetaTopicPub(topic);
+ UpdateMetaClientPub();
+
+ // acts as an announce + data update
+ SendAnnounce(topic, topic3->pubuid);
+ m_server.SetValue(this, topic, value);
+
+ // respond with assign message with assigned topic ID
+ if (m_local && m_state == kStateRunning) {
+ net3::WireEncodeEntryAssign(m_wire.Send().stream(), topic->name, topic->id,
+ topic3->seqNum.value(), value, topic3->flags);
+ } else {
+ m_outgoing.emplace_back(net3::Message3::EntryAssign(
+ topic->name, topic->id, topic3->seqNum.value(), value, topic3->flags));
+ }
+}
+
+void ClientData3::EntryUpdate(unsigned int id, unsigned int seq_num,
+ const Value& value) {
+ DEBUG4("EntryUpdate({}, {}, {}, {})", m_id, id, seq_num,
+ static_cast<int>(value.type()));
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received unexpected EntryUpdate message");
+ return;
+ }
+
+ if (id >= m_server.m_topics.size()) {
+ DEBUG3("ignored EntryUpdate from {} on non-existent topic {}", m_id, id);
+ return;
+ }
+ TopicData* topic = m_server.m_topics[id].get();
+ if (!topic || !topic->IsPublished()) {
+ DEBUG3("ignored EntryUpdate from {} on non-existent topic {}", m_id, id);
+ return;
+ }
+
+ TopicData3* topic3 = GetTopic3(topic);
+ if (!topic3->published) {
+ topic3->published = true;
+ topic3->pubuid = m_nextPubUid++;
+
+ // create publisher
+ auto [publisherIt, isNew] = m_publishers.try_emplace(
+ topic3->pubuid,
+ std::make_unique<PublisherData>(this, topic, topic3->pubuid));
+ if (isNew) {
+ // add publisher to topic
+ topic->publishers.Add(publisherIt->getSecond().get());
+
+ // update meta data
+ m_server.UpdateMetaTopicPub(topic);
+ UpdateMetaClientPub();
+ }
+ }
+ topic3->seqNum = net3::SequenceNumber{seq_num};
+
+ m_server.SetValue(this, topic, value);
+}
+
+void ClientData3::FlagsUpdate(unsigned int id, unsigned int flags) {
+ DEBUG4("FlagsUpdate({}, {}, {})", m_id, id, flags);
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received unexpected FlagsUpdate message");
+ return;
+ }
+ if (id >= m_server.m_topics.size()) {
+ DEBUG3("ignored FlagsUpdate from {} on non-existent topic {}", m_id, id);
+ return;
+ }
+ TopicData* topic = m_server.m_topics[id].get();
+ if (!topic || !topic->IsPublished()) {
+ DEBUG3("ignored FlagsUpdate from {} on non-existent topic {}", m_id, id);
+ return;
+ }
+ if (topic->special) {
+ DEBUG3("ignored FlagsUpdate from {} on special topic {}", m_id, id);
+ return;
+ }
+ m_server.SetFlags(this, topic, flags);
+}
+
+void ClientData3::EntryDelete(unsigned int id) {
+ DEBUG4("EntryDelete({}, {})", m_id, id);
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received unexpected EntryDelete message");
+ return;
+ }
+ if (id >= m_server.m_topics.size()) {
+ DEBUG3("ignored EntryDelete from {} on non-existent topic {}", m_id, id);
+ return;
+ }
+ TopicData* topic = m_server.m_topics[id].get();
+ if (!topic || !topic->IsPublished()) {
+ DEBUG3("ignored EntryDelete from {} on non-existent topic {}", m_id, id);
+ return;
+ }
+ if (topic->special) {
+ DEBUG3("ignored EntryDelete from {} on special topic {}", m_id, id);
+ return;
+ }
+
+ auto topic3it = m_topics3.find(topic);
+ if (topic3it != m_topics3.end()) {
+ // make sure we send assign the next time
+ topic3it->second.sentAssign = false;
+
+ // unpublish from this client (if it was previously published)
+ if (topic3it->second.published) {
+ topic3it->second.published = false;
+ auto publisherIt = m_publishers.find(topic3it->second.pubuid);
+ if (publisherIt != m_publishers.end()) {
+ // remove publisher from topic
+ topic->publishers.Remove(publisherIt->second.get());
+
+ // remove publisher from client
+ m_publishers.erase(publisherIt);
+
+ // update meta data
+ m_server.UpdateMetaTopicPub(topic);
+ UpdateMetaClientPub();
+ }
+ }
+ }
+
+ // set retained=false
+ m_server.SetProperties(this, topic, {{"retained", false}});
+}
+
+bool TopicData::SetProperties(const wpi::json& update) {
+ if (!update.is_object()) {
+ return false;
+ }
+ bool updated = false;
+ for (auto&& elem : update.items()) {
+ if (elem.value().is_null()) {
+ properties.erase(elem.key());
+ } else {
+ properties[elem.key()] = elem.value();
+ }
+ updated = true;
+ }
+ if (updated) {
+ RefreshProperties();
+ }
+ return updated;
+}
+
+void TopicData::RefreshProperties() {
+ persistent = false;
+ retained = false;
+
+ auto persistentIt = properties.find("persistent");
+ if (persistentIt != properties.end()) {
+ if (auto val = persistentIt->get_ptr<bool*>()) {
+ persistent = *val;
+ }
+ }
+
+ auto retainedIt = properties.find("retained");
+ if (retainedIt != properties.end()) {
+ if (auto val = retainedIt->get_ptr<bool*>()) {
+ retained = *val;
+ }
+ }
+}
+
+bool TopicData::SetFlags(unsigned int flags_) {
+ bool updated;
+ if ((flags_ & NT_PERSISTENT) != 0) {
+ updated = !persistent;
+ persistent = true;
+ properties["persistent"] = true;
+ } else {
+ updated = persistent;
+ persistent = false;
+ properties.erase("persistent");
+ }
+ return updated;
+}
+
+bool SubscriberData::Matches(std::string_view name, bool special) {
+ for (auto&& topicName : topicNames) {
+ if ((!options.prefixMatch && name == topicName) ||
+ (options.prefixMatch && (!special || !topicName.empty()) &&
+ wpi::starts_with(name, topicName))) {
+ return true;
+ }
+ }
+ return false;
+}
+
+SImpl::SImpl(wpi::Logger& logger) : m_logger{logger} {
+ // local is client 0
+ m_clients.emplace_back(std::make_unique<ClientDataLocal>(*this, 0, logger));
+ m_localClient = static_cast<ClientDataLocal*>(m_clients.back().get());
+}
+
+std::pair<std::string, int> SImpl::AddClient(
+ std::string_view name, std::string_view connInfo, bool local,
+ WireConnection& wire, ServerImpl::SetPeriodicFunc setPeriodic) {
+ // strip anything after @ in the name
+ name = wpi::split(name, '@').first;
+ if (name.empty()) {
+ name = "NT4";
+ }
+ size_t index = m_clients.size();
+ // find an empty slot and check for duplicates
+ // just do a linear search as number of clients is typically small (<10)
+ int duplicateName = 0;
+ for (size_t i = 0, end = index; i < end; ++i) {
+ auto& clientData = m_clients[i];
+ if (clientData && clientData->GetOriginalName() == name) {
+ ++duplicateName;
+ } else if (!clientData && index == end) {
+ index = i;
+ }
+ }
+ if (index == m_clients.size()) {
+ m_clients.emplace_back();
+ }
+
+ // if duplicate name, de-duplicate
+ std::string dedupName;
+ if (duplicateName > 0) {
+ dedupName = fmt::format("{}@{}", name, duplicateName);
+ } else {
+ dedupName = name;
+ }
+
+ auto& clientData = m_clients[index];
+ clientData = std::make_unique<ClientData4>(name, dedupName, connInfo, local,
+ wire, std::move(setPeriodic),
+ *this, index, m_logger);
+
+ // create client meta topics
+ clientData->m_metaPub =
+ CreateMetaTopic(fmt::format("$clientpub${}", dedupName));
+ clientData->m_metaSub =
+ CreateMetaTopic(fmt::format("$clientsub${}", dedupName));
+
+ // update meta topics
+ clientData->UpdateMetaClientPub();
+ clientData->UpdateMetaClientSub();
+
+ wire.Flush();
+
+ DEBUG3("AddClient('{}', '{}') -> {}", name, connInfo, index);
+ return {std::move(dedupName), index};
+}
+
+int SImpl::AddClient3(std::string_view connInfo, bool local,
+ net3::WireConnection3& wire,
+ ServerImpl::Connected3Func connected,
+ ServerImpl::SetPeriodicFunc setPeriodic) {
+ size_t index = m_clients.size();
+ // find an empty slot; we can't check for duplicates until we get a hello.
+ // just do a linear search as number of clients is typically small (<10)
+ for (size_t i = 0, end = index; i < end; ++i) {
+ if (!m_clients[i]) {
+ index = i;
+ break;
+ }
+ }
+ if (index == m_clients.size()) {
+ m_clients.emplace_back();
+ }
+
+ m_clients[index] = std::make_unique<ClientData3>(
+ connInfo, local, wire, std::move(connected), std::move(setPeriodic),
+ *this, index, m_logger);
+
+ DEBUG3("AddClient3('{}') -> {}", connInfo, index);
+ return index;
+}
+
+void SImpl::RemoveClient(int clientId) {
+ DEBUG3("RemoveClient({})", clientId);
+ auto& client = m_clients[clientId];
+
+ // remove all publishers and subscribers for this client
+ wpi::SmallVector<TopicData*, 16> toDelete;
+ for (auto&& topic : m_topics) {
+ auto pubRemove =
+ std::remove_if(topic->publishers.begin(), topic->publishers.end(),
+ [&](auto&& e) { return e->client == client.get(); });
+ bool pubChanged = pubRemove != topic->publishers.end();
+ topic->publishers.erase(pubRemove, topic->publishers.end());
+
+ auto subRemove =
+ std::remove_if(topic->subscribers.begin(), topic->subscribers.end(),
+ [&](auto&& e) { return e->client == client.get(); });
+ bool subChanged = subRemove != topic->subscribers.end();
+ topic->subscribers.erase(subRemove, topic->subscribers.end());
+
+ if (!topic->IsPublished()) {
+ toDelete.push_back(topic.get());
+ } else {
+ if (pubChanged) {
+ UpdateMetaTopicPub(topic.get());
+ }
+ if (subChanged) {
+ UpdateMetaTopicSub(topic.get());
+ }
+ }
+ }
+
+ // delete unpublished topics
+ for (auto topic : toDelete) {
+ DeleteTopic(topic);
+ }
+ DeleteTopic(client->m_metaPub);
+ DeleteTopic(client->m_metaSub);
+
+ // delete the client
+ client.reset();
+}
+
+bool SImpl::PersistentChanged() {
+ bool rv = m_persistentChanged;
+ m_persistentChanged = false;
+ return rv;
+}
+
+static void DumpValue(wpi::raw_ostream& os, const Value& value,
+ wpi::json::serializer& s) {
+ switch (value.type()) {
+ case NT_BOOLEAN:
+ if (value.GetBoolean()) {
+ os << "true";
+ } else {
+ os << "false";
+ }
+ break;
+ case NT_DOUBLE:
+ s.dump_float(value.GetDouble());
+ break;
+ case NT_FLOAT:
+ s.dump_float(value.GetFloat());
+ break;
+ case NT_INTEGER:
+ s.dump_integer(value.GetInteger());
+ break;
+ case NT_STRING:
+ os << '"';
+ s.dump_escaped(value.GetString(), false);
+ os << '"';
+ break;
+ case NT_RAW:
+ case NT_RPC:
+ os << '"';
+ wpi::Base64Encode(os, value.GetRaw());
+ os << '"';
+ break;
+ case NT_BOOLEAN_ARRAY: {
+ os << '[';
+ bool first = true;
+ for (auto v : value.GetBooleanArray()) {
+ if (first) {
+ first = false;
+ } else {
+ os << ", ";
+ }
+ if (v) {
+ os << "true";
+ } else {
+ os << "false";
+ }
+ }
+ os << ']';
+ break;
+ }
+ case NT_DOUBLE_ARRAY: {
+ os << '[';
+ bool first = true;
+ for (auto v : value.GetDoubleArray()) {
+ if (first) {
+ first = false;
+ } else {
+ os << ", ";
+ }
+ s.dump_float(v);
+ }
+ os << ']';
+ break;
+ }
+ case NT_FLOAT_ARRAY: {
+ os << '[';
+ bool first = true;
+ for (auto v : value.GetFloatArray()) {
+ if (first) {
+ first = false;
+ } else {
+ os << ", ";
+ }
+ s.dump_float(v);
+ }
+ os << ']';
+ break;
+ }
+ case NT_INTEGER_ARRAY: {
+ os << '[';
+ bool first = true;
+ for (auto v : value.GetIntegerArray()) {
+ if (first) {
+ first = false;
+ } else {
+ os << ", ";
+ }
+ s.dump_integer(v);
+ }
+ os << ']';
+ break;
+ }
+ case NT_STRING_ARRAY: {
+ os << '[';
+ bool first = true;
+ for (auto&& v : value.GetStringArray()) {
+ if (first) {
+ first = false;
+ } else {
+ os << ", ";
+ }
+ os << '"';
+ s.dump_escaped(v, false);
+ os << '"';
+ }
+ os << ']';
+ break;
+ }
+ default:
+ os << "null";
+ break;
+ }
+}
+
+void SImpl::DumpPersistent(wpi::raw_ostream& os) {
+ wpi::json::serializer s{os, ' ', 16};
+ os << "[\n";
+ bool first = true;
+ for (const auto& topic : m_topics) {
+ if (!topic->persistent || !topic->lastValue) {
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ os << ",\n";
+ }
+ os << " {\n \"name\": \"";
+ s.dump_escaped(topic->name, false);
+ os << "\",\n \"type\": \"";
+ s.dump_escaped(topic->typeStr, false);
+ os << "\",\n \"value\": ";
+ DumpValue(os, topic->lastValue, s);
+ os << ",\n \"properties\": ";
+ s.dump(topic->properties, true, false, 2, 4);
+ os << "\n }";
+ }
+ os << "\n]\n";
+}
+
+static std::string* ObjGetString(wpi::json::object_t& obj, std::string_view key,
+ std::string* error) {
+ auto it = obj.find(key);
+ if (it == obj.end()) {
+ *error = fmt::format("no {} key", key);
+ return nullptr;
+ }
+ auto val = it->second.get_ptr<std::string*>();
+ if (!val) {
+ *error = fmt::format("{} must be a string", key);
+ }
+ return val;
+}
+
+std::string SImpl::LoadPersistent(std::string_view in) {
+ if (in.empty()) {
+ return {};
+ }
+
+ wpi::json j;
+ try {
+ j = wpi::json::parse(in);
+ } catch (wpi::json::parse_error& err) {
+ return fmt::format("could not decode JSON: {}", err.what());
+ }
+
+ if (!j.is_array()) {
+ return "expected JSON array at top level";
+ }
+
+ bool persistentChanged = m_persistentChanged;
+
+ std::string allerrors;
+ int i = -1;
+ auto time = nt::Now();
+ for (auto&& jitem : j) {
+ ++i;
+ std::string error;
+ {
+ auto obj = jitem.get_ptr<wpi::json::object_t*>();
+ if (!obj) {
+ error = "expected item to be an object";
+ goto err;
+ }
+
+ // name
+ auto name = ObjGetString(*obj, "name", &error);
+ if (!name) {
+ goto err;
+ }
+
+ // type
+ auto typeStr = ObjGetString(*obj, "type", &error);
+ if (!typeStr) {
+ goto err;
+ }
+
+ // properties
+ auto propsIt = obj->find("properties");
+ if (propsIt == obj->end()) {
+ error = "no properties key";
+ goto err;
+ }
+ auto& props = propsIt->second;
+ if (!props.is_object()) {
+ error = "properties must be an object";
+ goto err;
+ }
+
+ // check to make sure persistent property is set
+ auto persistentIt = props.find("persistent");
+ if (persistentIt == props.end()) {
+ error = "no persistent property";
+ goto err;
+ }
+ if (auto v = persistentIt->get_ptr<bool*>()) {
+ if (!*v) {
+ error = "persistent property is false";
+ goto err;
+ }
+ } else {
+ error = "persistent property is not boolean";
+ goto err;
+ }
+
+ // value
+ auto valueIt = obj->find("value");
+ if (valueIt == obj->end()) {
+ error = "no value key";
+ goto err;
+ }
+ Value value;
+ if (*typeStr == "boolean") {
+ if (auto v = valueIt->second.get_ptr<bool*>()) {
+ value = Value::MakeBoolean(*v, time);
+ } else {
+ error = "value type mismatch, expected boolean";
+ goto err;
+ }
+ } else if (*typeStr == "int") {
+ if (auto v = valueIt->second.get_ptr<int64_t*>()) {
+ value = Value::MakeInteger(*v, time);
+ } else if (auto v = valueIt->second.get_ptr<uint64_t*>()) {
+ value = Value::MakeInteger(*v, time);
+ } else {
+ error = "value type mismatch, expected int";
+ goto err;
+ }
+ } else if (*typeStr == "float") {
+ if (auto v = valueIt->second.get_ptr<double*>()) {
+ value = Value::MakeFloat(*v, time);
+ } else {
+ error = "value type mismatch, expected float";
+ goto err;
+ }
+ } else if (*typeStr == "double") {
+ if (auto v = valueIt->second.get_ptr<double*>()) {
+ value = Value::MakeDouble(*v, time);
+ } else {
+ error = "value type mismatch, expected double";
+ goto err;
+ }
+ } else if (*typeStr == "string" || *typeStr == "json") {
+ if (auto v = valueIt->second.get_ptr<std::string*>()) {
+ value = Value::MakeString(*v, time);
+ } else {
+ error = "value type mismatch, expected string";
+ goto err;
+ }
+ } else if (*typeStr == "boolean[]") {
+ auto arr = valueIt->second.get_ptr<wpi::json::array_t*>();
+ if (!arr) {
+ error = "value type mismatch, expected array";
+ goto err;
+ }
+ std::vector<int> elems;
+ for (auto&& jelem : valueIt->second) {
+ if (auto v = jelem.get_ptr<bool*>()) {
+ elems.push_back(*v);
+ } else {
+ error = "value type mismatch, expected boolean";
+ }
+ }
+ value = Value::MakeBooleanArray(elems, time);
+ } else if (*typeStr == "int[]") {
+ auto arr = valueIt->second.get_ptr<wpi::json::array_t*>();
+ if (!arr) {
+ error = "value type mismatch, expected array";
+ goto err;
+ }
+ std::vector<int64_t> elems;
+ for (auto&& jelem : valueIt->second) {
+ if (auto v = jelem.get_ptr<int64_t*>()) {
+ elems.push_back(*v);
+ } else if (auto v = jelem.get_ptr<uint64_t*>()) {
+ elems.push_back(*v);
+ } else {
+ error = "value type mismatch, expected int";
+ }
+ }
+ value = Value::MakeIntegerArray(elems, time);
+ } else if (*typeStr == "double[]") {
+ auto arr = valueIt->second.get_ptr<wpi::json::array_t*>();
+ if (!arr) {
+ error = "value type mismatch, expected array";
+ goto err;
+ }
+ std::vector<double> elems;
+ for (auto&& jelem : valueIt->second) {
+ if (auto v = jelem.get_ptr<double*>()) {
+ elems.push_back(*v);
+ } else {
+ error = "value type mismatch, expected double";
+ }
+ }
+ value = Value::MakeDoubleArray(elems, time);
+ } else if (*typeStr == "float[]") {
+ auto arr = valueIt->second.get_ptr<wpi::json::array_t*>();
+ if (!arr) {
+ error = "value type mismatch, expected array";
+ goto err;
+ }
+ std::vector<float> elems;
+ for (auto&& jelem : valueIt->second) {
+ if (auto v = jelem.get_ptr<double*>()) {
+ elems.push_back(*v);
+ } else {
+ error = "value type mismatch, expected float";
+ }
+ }
+ value = Value::MakeFloatArray(elems, time);
+ } else if (*typeStr == "string[]") {
+ auto arr = valueIt->second.get_ptr<wpi::json::array_t*>();
+ if (!arr) {
+ error = "value type mismatch, expected array";
+ goto err;
+ }
+ std::vector<std::string> elems;
+ for (auto&& jelem : valueIt->second) {
+ if (auto v = jelem.get_ptr<std::string*>()) {
+ elems.emplace_back(*v);
+ } else {
+ error = "value type mismatch, expected string";
+ }
+ }
+ value = Value::MakeStringArray(std::move(elems), time);
+ } else {
+ // raw
+ if (auto v = valueIt->second.get_ptr<std::string*>()) {
+ std::vector<uint8_t> data;
+ wpi::Base64Decode(*v, &data);
+ value = Value::MakeRaw(std::move(data), time);
+ } else {
+ error = "value type mismatch, expected string";
+ goto err;
+ }
+ }
+
+ // create persistent topic
+ auto topic = CreateTopic(nullptr, *name, *typeStr, props);
+
+ // set value
+ SetValue(nullptr, topic, value);
+
+ continue;
+ }
+ err:
+ allerrors += fmt::format("{}: {}\n", i, error);
+ }
+
+ m_persistentChanged = persistentChanged; // restore flag
+
+ return allerrors;
+}
+
+TopicData* SImpl::CreateTopic(ClientData* client, std::string_view name,
+ std::string_view typeStr,
+ const wpi::json& properties, bool special) {
+ auto& topic = m_nameTopics[name];
+ if (topic) {
+ if (typeStr != topic->typeStr) {
+ if (client) {
+ WARNING("client {} publish '{}' conflicting type '{}' (currently '{}')",
+ client->GetName(), name, typeStr, topic->typeStr);
+ }
+ }
+ } else {
+ // new topic
+ unsigned int id = m_topics.emplace_back(
+ std::make_unique<TopicData>(name, typeStr, properties));
+ topic = m_topics[id].get();
+ topic->id = id;
+ topic->special = special;
+
+ for (auto&& aClient : m_clients) {
+ if (!aClient) {
+ continue;
+ }
+
+ // look for subscriber matching prefixes
+ wpi::SmallVector<SubscriberData*, 16> subscribersBuf;
+ auto subscribers =
+ aClient->GetSubscribers(name, topic->special, subscribersBuf);
+ for (auto subscriber : subscribers) {
+ topic->subscribers.Add(subscriber);
+ }
+
+ // don't announce to this client if no subscribers
+ if (subscribers.empty()) {
+ continue;
+ }
+
+ if (aClient.get() == client) {
+ continue; // don't announce to requesting client again
+ }
+
+ DEBUG4("client {}: announce {}", aClient->GetId(), topic->name);
+ aClient->SendAnnounce(topic, std::nullopt);
+ }
+
+ // create meta topics; don't create if topic is itself a meta topic
+ if (!special) {
+ topic->metaPub = CreateMetaTopic(fmt::format("$pub${}", name));
+ topic->metaSub = CreateMetaTopic(fmt::format("$sub${}", name));
+ UpdateMetaTopicPub(topic);
+ UpdateMetaTopicSub(topic);
+ }
+ }
+
+ return topic;
+}
+
+TopicData* SImpl::CreateMetaTopic(std::string_view name) {
+ return CreateTopic(nullptr, name, "msgpack", {{"retained", true}}, true);
+}
+
+void SImpl::DeleteTopic(TopicData* topic) {
+ if (!topic) {
+ return;
+ }
+
+ // delete meta topics
+ if (topic->metaPub) {
+ DeleteTopic(topic->metaPub);
+ }
+ if (topic->metaSub) {
+ DeleteTopic(topic->metaSub);
+ }
+
+ // unannounce to all subscribers
+ wpi::SmallVector<bool, 16> clients;
+ clients.resize(m_clients.size());
+ for (auto&& sub : topic->subscribers) {
+ clients[sub->client->GetId()] = true;
+ }
+ for (size_t i = 0, iend = clients.size(); i < iend; ++i) {
+ if (!clients[i]) {
+ continue;
+ }
+ if (auto aClient = m_clients[i].get()) {
+ aClient->SendUnannounce(topic);
+ }
+ }
+
+ // erase the topic
+ m_nameTopics.erase(topic->name);
+ m_topics.erase(topic->id);
+}
+
+void SImpl::SetProperties(ClientData* client, TopicData* topic,
+ const wpi::json& update) {
+ DEBUG4("SetProperties({}, {}, {})", client ? client->GetId() : -1,
+ topic->name, update.dump());
+ bool wasPersistent = topic->persistent;
+ if (topic->SetProperties(update)) {
+ // update persistentChanged flag
+ if (topic->persistent != wasPersistent) {
+ m_persistentChanged = true;
+ }
+ PropertiesChanged(client, topic, update);
+ }
+}
+
+void SImpl::SetFlags(ClientData* client, TopicData* topic, unsigned int flags) {
+ bool wasPersistent = topic->persistent;
+ if (topic->SetFlags(flags)) {
+ // update persistentChanged flag
+ if (topic->persistent != wasPersistent) {
+ m_persistentChanged = true;
+ wpi::json update;
+ if (topic->persistent) {
+ update = {{"persistent", true}};
+ } else {
+ update = {{"persistent", wpi::json::object()}};
+ }
+ PropertiesChanged(client, topic, update);
+ }
+ }
+}
+
+void SImpl::SetValue(ClientData* client, TopicData* topic, const Value& value) {
+ // update retained value if from same client or timestamp newer
+ if (!topic->lastValue || topic->lastValueClient == client ||
+ value.time() >= topic->lastValue.time()) {
+ DEBUG4("updating '{}' last value (time was {} is {})", topic->name,
+ topic->lastValue.time(), value.time());
+ topic->lastValue = value;
+ topic->lastValueClient = client;
+
+ // if persistent, update flag
+ if (topic->persistent) {
+ m_persistentChanged = true;
+ }
+ }
+
+ // propagate to subscribers; as each client may have multiple subscribers,
+ // but we only want to send the value once, first map to clients and then
+ // take action based on union of subscriptions
+
+ // indexed by clientId
+ wpi::SmallVector<ClientData::SendMode, 16> toSend;
+ toSend.resize(m_clients.size());
+
+ for (auto&& subscriber : topic->subscribers) {
+ int id = subscriber->client->GetId();
+ if (subscriber->options.topicsOnly) {
+ continue;
+ } else if (subscriber->options.sendAll) {
+ toSend[id] = ClientData::kSendAll;
+ } else if (toSend[id] != ClientData::kSendAll) {
+ toSend[id] = ClientData::kSendNormal;
+ }
+ }
+
+ for (size_t i = 0, iend = toSend.size(); i < iend; ++i) {
+ auto aClient = m_clients[i].get();
+ if (!aClient || client == aClient) {
+ continue; // don't echo back
+ }
+ if (toSend[i] != ClientData::kSendDisabled) {
+ aClient->SendValue(topic, value, toSend[i]);
+ }
+ }
+}
+
+void SImpl::UpdateMetaClients(const std::vector<ConnectionInfo>& conns) {
+ Writer w;
+ mpack_start_array(&w, conns.size());
+ for (auto&& conn : conns) {
+ mpack_start_map(&w, 3);
+ mpack_write_str(&w, "id");
+ mpack_write_str(&w, conn.remote_id);
+ mpack_write_str(&w, "conn");
+ mpack_write_str(&w, fmt::format("{}:{}", conn.remote_ip, conn.remote_port));
+ mpack_write_str(&w, "ver");
+ mpack_write_u16(&w, conn.protocol_version);
+ mpack_finish_map(&w);
+ }
+ mpack_finish_array(&w);
+ if (mpack_writer_destroy(&w) == mpack_ok) {
+ SetValue(nullptr, m_metaClients, Value::MakeRaw(std::move(w.bytes)));
+ } else {
+ DEBUG4("failed to encode $clients");
+ }
+}
+
+void SImpl::UpdateMetaTopicPub(TopicData* topic) {
+ if (!topic->metaPub) {
+ return;
+ }
+ Writer w;
+ mpack_start_array(&w, topic->publishers.size());
+ for (auto&& pub : topic->publishers) {
+ mpack_start_map(&w, 2);
+ mpack_write_str(&w, "client");
+ if (pub->client) {
+ mpack_write_str(&w, pub->client->GetName());
+ } else {
+ mpack_write_str(&w, "");
+ }
+ mpack_write_str(&w, "pubuid");
+ mpack_write_int(&w, pub->pubuid);
+ mpack_finish_map(&w);
+ }
+ mpack_finish_array(&w);
+ if (mpack_writer_destroy(&w) == mpack_ok) {
+ SetValue(nullptr, topic->metaPub, Value::MakeRaw(std::move(w.bytes)));
+ }
+}
+
+void SImpl::UpdateMetaTopicSub(TopicData* topic) {
+ if (!topic->metaSub) {
+ return;
+ }
+ Writer w;
+ mpack_start_array(&w, topic->subscribers.size());
+ for (auto&& sub : topic->subscribers) {
+ mpack_start_map(&w, 3);
+ mpack_write_str(&w, "client");
+ if (sub->client) {
+ mpack_write_str(&w, sub->client->GetName());
+ } else {
+ mpack_write_str(&w, "");
+ }
+ mpack_write_str(&w, "subuid");
+ mpack_write_int(&w, sub->subuid);
+ mpack_write_str(&w, "options");
+ WriteOptions(w, sub->options);
+ mpack_finish_map(&w);
+ }
+ mpack_finish_array(&w);
+ if (mpack_writer_destroy(&w) == mpack_ok) {
+ SetValue(nullptr, topic->metaSub, Value::MakeRaw(std::move(w.bytes)));
+ }
+}
+
+void SImpl::PropertiesChanged(ClientData* client, TopicData* topic,
+ const wpi::json& update) {
+ // removing some properties can result in the topic being unpublished
+ if (!topic->IsPublished()) {
+ DeleteTopic(topic);
+ } else {
+ // send updated announcement to all subscribers
+ wpi::SmallVector<bool, 16> clients;
+ clients.resize(m_clients.size());
+ for (auto&& sub : topic->subscribers) {
+ clients[sub->client->GetId()] = true;
+ }
+ for (size_t i = 0, iend = clients.size(); i < iend; ++i) {
+ if (!clients[i]) {
+ continue;
+ }
+ if (auto aClient = m_clients[i].get()) {
+ aClient->SendPropertiesUpdate(topic, update, aClient == client);
+ }
+ }
+ }
+}
+
+class ServerImpl::Impl final : public SImpl {
+ public:
+ explicit Impl(wpi::Logger& logger) : SImpl{logger} {}
+};
+
+ServerImpl::ServerImpl(wpi::Logger& logger)
+ : m_impl{std::make_unique<Impl>(logger)} {}
+
+ServerImpl::~ServerImpl() = default;
+
+void ServerImpl::SendControl(uint64_t curTimeMs) {
+ if (!m_impl->m_controlReady) {
+ return;
+ }
+ m_impl->m_controlReady = false;
+
+ for (auto&& client : m_impl->m_clients) {
+ if (client) {
+ // to ensure ordering, just send everything
+ client->SendOutgoing(curTimeMs);
+ client->Flush();
+ }
+ }
+}
+
+void ServerImpl::SendValues(int clientId, uint64_t curTimeMs) {
+ auto client = m_impl->m_clients[clientId].get();
+ client->SendOutgoing(curTimeMs);
+ client->Flush();
+}
+
+void ServerImpl::HandleLocal(std::span<const ClientMessage> msgs) {
+ // just map as a normal client into client=0 calls
+ m_impl->m_localClient->HandleLocal(msgs);
+}
+
+void ServerImpl::SetLocal(LocalInterface* local) {
+ WPI_DEBUG4(m_impl->m_logger, "SetLocal()");
+ m_impl->m_local = local;
+
+ // create server meta topics
+ m_impl->m_metaClients = m_impl->CreateMetaTopic("$clients");
+
+ // create local client meta topics
+ m_impl->m_localClient->m_metaPub = m_impl->CreateMetaTopic("$serverpub");
+ m_impl->m_localClient->m_metaSub = m_impl->CreateMetaTopic("$serversub");
+
+ // update meta topics
+ m_impl->m_localClient->UpdateMetaClientPub();
+ m_impl->m_localClient->UpdateMetaClientSub();
+}
+
+void ServerImpl::ProcessIncomingText(int clientId, std::string_view data) {
+ m_impl->m_clients[clientId]->ProcessIncomingText(data);
+}
+
+void ServerImpl::ProcessIncomingBinary(int clientId,
+ std::span<const uint8_t> data) {
+ m_impl->m_clients[clientId]->ProcessIncomingBinary(data);
+}
+
+std::pair<std::string, int> ServerImpl::AddClient(std::string_view name,
+ std::string_view connInfo,
+ bool local,
+ WireConnection& wire,
+ SetPeriodicFunc setPeriodic) {
+ return m_impl->AddClient(name, connInfo, local, wire, std::move(setPeriodic));
+}
+
+int ServerImpl::AddClient3(std::string_view connInfo, bool local,
+ net3::WireConnection3& wire,
+ Connected3Func connected,
+ SetPeriodicFunc setPeriodic) {
+ return m_impl->AddClient3(connInfo, local, wire, std::move(connected),
+ std::move(setPeriodic));
+}
+
+void ServerImpl::RemoveClient(int clientId) {
+ m_impl->RemoveClient(clientId);
+}
+
+void ServerImpl::ConnectionsChanged(const std::vector<ConnectionInfo>& conns) {
+ m_impl->UpdateMetaClients(conns);
+}
+
+bool ServerImpl::PersistentChanged() {
+ return m_impl->PersistentChanged();
+}
+
+std::string ServerImpl::DumpPersistent() {
+ std::string rv;
+ wpi::raw_string_ostream os{rv};
+ m_impl->DumpPersistent(os);
+ os.flush();
+ return rv;
+}
+
+std::string ServerImpl::LoadPersistent(std::string_view in) {
+ return m_impl->LoadPersistent(in);
+}
diff --git a/ntcore/src/main/native/cpp/net/ServerImpl.h b/ntcore/src/main/native/cpp/net/ServerImpl.h
new file mode 100644
index 0000000..86607e9
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/ServerImpl.h
@@ -0,0 +1,76 @@
+// 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 <functional>
+#include <memory>
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "NetworkInterface.h"
+#include "net3/WireConnection3.h"
+
+namespace wpi {
+class Logger;
+} // namespace wpi
+
+namespace nt::net3 {
+class WireConnection3;
+} // namespace nt::net3
+
+namespace nt::net {
+
+struct ClientMessage;
+class LocalInterface;
+class WireConnection;
+
+class ServerImpl final {
+ public:
+ using SetPeriodicFunc = std::function<void(uint32_t repeatMs)>;
+ using Connected3Func =
+ std::function<void(std::string_view name, uint16_t proto)>;
+
+ explicit ServerImpl(wpi::Logger& logger);
+ ~ServerImpl();
+
+ void SendControl(uint64_t curTimeMs);
+ void SendValues(int clientId, uint64_t curTimeMs);
+
+ void HandleLocal(std::span<const ClientMessage> msgs);
+ void SetLocal(LocalInterface* local);
+
+ void ProcessIncomingText(int clientId, std::string_view data);
+ void ProcessIncomingBinary(int clientId, std::span<const uint8_t> data);
+
+ // Returns -1 if cannot add client (e.g. due to duplicate name).
+ // Caller must ensure WireConnection lifetime lasts until RemoveClient() call.
+ std::pair<std::string, int> AddClient(std::string_view name,
+ std::string_view connInfo, bool local,
+ WireConnection& wire,
+ SetPeriodicFunc setPeriodic);
+ int AddClient3(std::string_view connInfo, bool local,
+ net3::WireConnection3& wire, Connected3Func connected,
+ SetPeriodicFunc setPeriodic);
+ void RemoveClient(int clientId);
+
+ void ConnectionsChanged(const std::vector<ConnectionInfo>& conns);
+
+ // if any persistent values changed since the last call to this function
+ bool PersistentChanged();
+ std::string DumpPersistent();
+ // returns newline-separated errors
+ std::string LoadPersistent(std::string_view in);
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> m_impl;
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp b/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp
new file mode 100644
index 0000000..d3d192f
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp
@@ -0,0 +1,126 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "WebSocketConnection.h"
+
+#include <span>
+
+#include <wpi/SpanExtras.h>
+#include <wpinet/WebSocket.h>
+
+using namespace nt;
+using namespace nt::net;
+
+static constexpr size_t kAllocSize = 4096;
+static constexpr size_t kTextFrameRolloverSize = 4096;
+static constexpr size_t kBinaryFrameRolloverSize = 8192;
+
+WebSocketConnection::WebSocketConnection(wpi::WebSocket& ws)
+ : m_ws{ws},
+ m_text_os{m_text_buffers, [this] { return AllocBuf(); }},
+ m_binary_os{m_binary_buffers, [this] { return AllocBuf(); }} {}
+
+WebSocketConnection::~WebSocketConnection() {
+ for (auto&& buf : m_buf_pool) {
+ buf.Deallocate();
+ }
+}
+
+void WebSocketConnection::Flush() {
+ FinishSendText();
+ FinishSendBinary();
+ if (m_frames.empty()) {
+ return;
+ }
+
+ // convert internal frames into WS frames
+ m_ws_frames.clear();
+ m_ws_frames.reserve(m_frames.size());
+ for (auto&& frame : m_frames) {
+ m_ws_frames.emplace_back(frame.opcode,
+ std::span{frame.bufs->begin() + frame.start,
+ frame.bufs->begin() + frame.end});
+ }
+
+ ++m_sendsActive;
+ m_ws.SendFrames(m_ws_frames, [selfweak = weak_from_this()](auto bufs, auto) {
+ if (auto self = selfweak.lock()) {
+ self->m_buf_pool.insert(self->m_buf_pool.end(), bufs.begin(), bufs.end());
+ if (self->m_sendsActive > 0) {
+ --self->m_sendsActive;
+ }
+ }
+ });
+ m_frames.clear();
+ m_text_buffers.clear();
+ m_binary_buffers.clear();
+ m_text_pos = 0;
+ m_binary_pos = 0;
+}
+
+void WebSocketConnection::Disconnect(std::string_view reason) {
+ m_ws.Close(1005, reason);
+}
+
+void WebSocketConnection::StartSendText() {
+ // limit amount per single frame
+ size_t total = 0;
+ for (size_t i = m_text_pos; i < m_text_buffers.size(); ++i) {
+ total += m_text_buffers[i].len;
+ }
+ if (total >= kTextFrameRolloverSize) {
+ FinishSendText();
+ }
+
+ if (m_in_text) {
+ m_text_os << ',';
+ } else {
+ m_text_os << '[';
+ m_in_text = true;
+ }
+}
+
+void WebSocketConnection::FinishSendText() {
+ if (m_in_text) {
+ m_text_os << ']';
+ m_in_text = false;
+ }
+ if (m_text_pos >= m_text_buffers.size()) {
+ return;
+ }
+ m_frames.emplace_back(wpi::WebSocket::Frame::kText, &m_text_buffers,
+ m_text_pos, m_text_buffers.size());
+ m_text_pos = m_text_buffers.size();
+ m_text_os.reset();
+}
+
+void WebSocketConnection::StartSendBinary() {
+ // limit amount per single frame
+ size_t total = 0;
+ for (size_t i = m_binary_pos; i < m_binary_buffers.size(); ++i) {
+ total += m_binary_buffers[i].len;
+ }
+ if (total >= kBinaryFrameRolloverSize) {
+ FinishSendBinary();
+ }
+}
+
+void WebSocketConnection::FinishSendBinary() {
+ if (m_binary_pos >= m_binary_buffers.size()) {
+ return;
+ }
+ m_frames.emplace_back(wpi::WebSocket::Frame::kBinary, &m_binary_buffers,
+ m_binary_pos, m_binary_buffers.size());
+ m_binary_pos = m_binary_buffers.size();
+ m_binary_os.reset();
+}
+
+wpi::uv::Buffer WebSocketConnection::AllocBuf() {
+ if (!m_buf_pool.empty()) {
+ auto buf = m_buf_pool.back();
+ m_buf_pool.pop_back();
+ return buf;
+ }
+ return wpi::uv::Buffer::Allocate(kAllocSize);
+}
diff --git a/ntcore/src/main/native/cpp/net/WebSocketConnection.h b/ntcore/src/main/native/cpp/net/WebSocketConnection.h
new file mode 100644
index 0000000..ba15215
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/WebSocketConnection.h
@@ -0,0 +1,69 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include <wpi/SmallVector.h>
+#include <wpinet/WebSocket.h>
+#include <wpinet/raw_uv_ostream.h>
+#include <wpinet/uv/Buffer.h>
+
+#include "WireConnection.h"
+
+namespace nt::net {
+
+class WebSocketConnection final
+ : public WireConnection,
+ public std::enable_shared_from_this<WebSocketConnection> {
+ public:
+ explicit WebSocketConnection(wpi::WebSocket& ws);
+ ~WebSocketConnection() override;
+ WebSocketConnection(const WebSocketConnection&) = delete;
+ WebSocketConnection& operator=(const WebSocketConnection&) = delete;
+
+ bool Ready() const final { return m_sendsActive == 0; }
+
+ TextWriter SendText() final { return {m_text_os, *this}; }
+ BinaryWriter SendBinary() final { return {m_binary_os, *this}; }
+
+ void Flush() final;
+
+ void Disconnect(std::string_view reason) final;
+
+ private:
+ void StartSendText() final;
+ void FinishSendText() final;
+ void StartSendBinary() final;
+ void FinishSendBinary() final;
+
+ wpi::uv::Buffer AllocBuf();
+
+ wpi::WebSocket& m_ws;
+ // Can't use WS frames directly as span could have dangling pointers
+ struct Frame {
+ Frame(uint8_t opcode, wpi::SmallVectorImpl<wpi::uv::Buffer>* bufs,
+ size_t start, size_t end)
+ : opcode{opcode}, bufs{bufs}, start{start}, end{end} {}
+ uint8_t opcode;
+ wpi::SmallVectorImpl<wpi::uv::Buffer>* bufs;
+ size_t start;
+ size_t end;
+ };
+ std::vector<Frame> m_frames;
+ std::vector<wpi::WebSocket::Frame> m_ws_frames; // to reduce allocs
+ wpi::SmallVector<wpi::uv::Buffer, 4> m_text_buffers;
+ wpi::SmallVector<wpi::uv::Buffer, 4> m_binary_buffers;
+ std::vector<wpi::uv::Buffer> m_buf_pool;
+ wpi::raw_uv_ostream m_text_os;
+ wpi::raw_uv_ostream m_binary_os;
+ size_t m_text_pos = 0;
+ size_t m_binary_pos = 0;
+ bool m_in_text = false;
+ int m_sendsActive = 0;
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/WireConnection.h b/ntcore/src/main/native/cpp/net/WireConnection.h
new file mode 100644
index 0000000..2a79a12
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/WireConnection.h
@@ -0,0 +1,110 @@
+// 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 <string_view>
+
+#include <wpi/raw_ostream.h>
+
+namespace nt::net {
+
+class BinaryWriter;
+class TextWriter;
+
+class WireConnection {
+ friend class TextWriter;
+ friend class BinaryWriter;
+
+ public:
+ virtual ~WireConnection() = default;
+
+ virtual bool Ready() const = 0;
+
+ virtual TextWriter SendText() = 0;
+
+ virtual BinaryWriter SendBinary() = 0;
+
+ virtual void Flush() = 0;
+
+ virtual void Disconnect(std::string_view reason) = 0;
+
+ protected:
+ virtual void StartSendText() = 0;
+ virtual void FinishSendText() = 0;
+ virtual void StartSendBinary() = 0;
+ virtual void FinishSendBinary() = 0;
+};
+
+class TextWriter {
+ public:
+ TextWriter(wpi::raw_ostream& os, WireConnection& wire)
+ : m_os{&os}, m_wire{&wire} {}
+ TextWriter(const TextWriter&) = delete;
+ TextWriter(TextWriter&& rhs) : m_os{rhs.m_os}, m_wire{rhs.m_wire} {
+ rhs.m_os = nullptr;
+ rhs.m_wire = nullptr;
+ }
+ TextWriter& operator=(const TextWriter&) = delete;
+ TextWriter& operator=(TextWriter&& rhs) {
+ m_os = rhs.m_os;
+ m_wire = rhs.m_wire;
+ rhs.m_os = nullptr;
+ rhs.m_wire = nullptr;
+ return *this;
+ }
+ ~TextWriter() {
+ if (m_os) {
+ m_wire->FinishSendText();
+ }
+ }
+
+ wpi::raw_ostream& Add() {
+ m_wire->StartSendText();
+ return *m_os;
+ }
+ WireConnection& wire() { return *m_wire; }
+
+ private:
+ wpi::raw_ostream* m_os;
+ WireConnection* m_wire;
+};
+
+class BinaryWriter {
+ public:
+ BinaryWriter(wpi::raw_ostream& os, WireConnection& wire)
+ : m_os{&os}, m_wire{&wire} {}
+ BinaryWriter(const BinaryWriter&) = delete;
+ BinaryWriter(BinaryWriter&& rhs) : m_os{rhs.m_os}, m_wire{rhs.m_wire} {
+ rhs.m_os = nullptr;
+ rhs.m_wire = nullptr;
+ }
+ BinaryWriter& operator=(const BinaryWriter&) = delete;
+ BinaryWriter& operator=(BinaryWriter&& rhs) {
+ m_os = rhs.m_os;
+ m_wire = rhs.m_wire;
+ rhs.m_os = nullptr;
+ rhs.m_wire = nullptr;
+ return *this;
+ }
+ ~BinaryWriter() {
+ if (m_wire) {
+ m_wire->FinishSendBinary();
+ }
+ }
+
+ wpi::raw_ostream& Add() {
+ m_wire->StartSendBinary();
+ return *m_os;
+ }
+ WireConnection& wire() { return *m_wire; }
+
+ private:
+ wpi::raw_ostream* m_os;
+ WireConnection* m_wire;
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/WireDecoder.cpp b/ntcore/src/main/native/cpp/net/WireDecoder.cpp
new file mode 100644
index 0000000..e6474b2
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/WireDecoder.cpp
@@ -0,0 +1,567 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "WireDecoder.h"
+
+#include <algorithm>
+
+#include <fmt/format.h>
+#include <wpi/Logger.h>
+#include <wpi/SpanExtras.h>
+#include <wpi/json.h>
+#include <wpi/mpack.h>
+
+#include "Message.h"
+
+using namespace nt;
+using namespace nt::net;
+using namespace mpack;
+
+static bool GetNumber(wpi::json& val, double* num) {
+ if (auto v = val.get_ptr<const int64_t*>()) {
+ *num = *v;
+ } else if (auto v = val.get_ptr<const uint64_t*>()) {
+ *num = *v;
+ } else if (auto v = val.get_ptr<const double*>()) {
+ *num = *v;
+ } else {
+ return false;
+ }
+ return true;
+}
+
+static bool GetNumber(wpi::json& val, int64_t* num) {
+ if (auto v = val.get_ptr<const int64_t*>()) {
+ *num = *v;
+ } else if (auto v = val.get_ptr<const uint64_t*>()) {
+ *num = *v;
+ } else {
+ return false;
+ }
+ return true;
+}
+
+static std::string* ObjGetString(wpi::json::object_t& obj, std::string_view key,
+ std::string* error) {
+ auto it = obj.find(key);
+ if (it == obj.end()) {
+ *error = fmt::format("no {} key", key);
+ return nullptr;
+ }
+ auto val = it->second.get_ptr<std::string*>();
+ if (!val) {
+ *error = fmt::format("{} must be a string", key);
+ }
+ return val;
+}
+
+static bool ObjGetNumber(wpi::json::object_t& obj, std::string_view key,
+ std::string* error, int64_t* num) {
+ auto it = obj.find(key);
+ if (it == obj.end()) {
+ *error = fmt::format("no {} key", key);
+ return false;
+ }
+ if (!GetNumber(it->second, num)) {
+ *error = fmt::format("{} must be a number", key);
+ return false;
+ }
+ return true;
+}
+
+static bool ObjGetStringArray(wpi::json::object_t& obj, std::string_view key,
+ std::string* error,
+ std::vector<std::string>* out) {
+ // prefixes
+ auto it = obj.find(key);
+ if (it == obj.end()) {
+ *error = fmt::format("no {} key", key);
+ return false;
+ }
+ auto jarr = it->second.get_ptr<wpi::json::array_t*>();
+ if (!jarr) {
+ *error = fmt::format("{} must be an array", key);
+ return false;
+ }
+ out->resize(0);
+ out->reserve(jarr->size());
+ for (auto&& jval : *jarr) {
+ auto str = jval.get_ptr<std::string*>();
+ if (!str) {
+ *error = fmt::format("{}/{} must be a string", key, out->size());
+ return false;
+ }
+ out->emplace_back(std::move(*str));
+ }
+ return true;
+}
+
+// avoid a fmtlib "unused type alias 'char_type'" warning false positive
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-local-typedef"
+#endif
+
+template <typename T>
+static void WireDecodeTextImpl(std::string_view in, T& out,
+ wpi::Logger& logger) {
+ static_assert(std::is_same_v<T, ClientMessageHandler> ||
+ std::is_same_v<T, ServerMessageHandler>,
+ "T must be ClientMessageHandler or ServerMessageHandler");
+
+ wpi::json j;
+ try {
+ j = wpi::json::parse(in);
+ } catch (wpi::json::parse_error& err) {
+ WPI_WARNING(logger, "could not decode JSON message: {}", err.what());
+ return;
+ }
+
+ if (!j.is_array()) {
+ WPI_WARNING(logger, "expected JSON array at top level");
+ return;
+ }
+
+ int i = -1;
+ for (auto&& jmsg : j) {
+ ++i;
+ std::string error;
+ {
+ auto obj = jmsg.get_ptr<wpi::json::object_t*>();
+ if (!obj) {
+ error = "expected message to be an object";
+ goto err;
+ }
+
+ auto method = ObjGetString(*obj, "method", &error);
+ if (!method) {
+ goto err;
+ }
+
+ auto paramsIt = obj->find("params");
+ if (paramsIt == obj->end()) {
+ error = "no params key";
+ goto err;
+ }
+ auto params = paramsIt->second.get_ptr<wpi::json::object_t*>();
+ if (!params) {
+ error = "params must be an object";
+ goto err;
+ }
+
+ if constexpr (std::is_same_v<T, ClientMessageHandler>) {
+ if (*method == PublishMsg::kMethodStr) {
+ // name
+ auto name = ObjGetString(*params, "name", &error);
+ if (!name) {
+ goto err;
+ }
+
+ // type
+ auto typeStr = ObjGetString(*params, "type", &error);
+ if (!typeStr) {
+ goto err;
+ }
+
+ // pubuid
+ int64_t pubuid;
+ if (!ObjGetNumber(*params, "pubuid", &error, &pubuid)) {
+ goto err;
+ }
+
+ // properties; allow missing (treated as empty)
+ wpi::json* properties = nullptr;
+ auto propertiesIt = params->find("properties");
+ if (propertiesIt != params->end()) {
+ properties = &propertiesIt->second;
+ if (!properties->is_object()) {
+ error = "properties must be an object";
+ goto err;
+ }
+ }
+ wpi::json propertiesEmpty;
+ if (!properties) {
+ propertiesEmpty = wpi::json::object();
+ properties = &propertiesEmpty;
+ }
+
+ // complete
+ out.ClientPublish(pubuid, *name, *typeStr, *properties);
+ } else if (*method == UnpublishMsg::kMethodStr) {
+ // pubuid
+ int64_t pubuid;
+ if (!ObjGetNumber(*params, "pubuid", &error, &pubuid)) {
+ goto err;
+ }
+
+ // complete
+ out.ClientUnpublish(pubuid);
+ } else if (*method == SetPropertiesMsg::kMethodStr) {
+ // name
+ auto name = ObjGetString(*params, "name", &error);
+ if (!name) {
+ goto err;
+ }
+
+ // update
+ auto updateIt = params->find("update");
+ if (updateIt == params->end()) {
+ error = "no update key";
+ goto err;
+ }
+ auto update = &updateIt->second;
+ if (!update->is_object()) {
+ error = "update must be an object";
+ goto err;
+ }
+
+ // complete
+ out.ClientSetProperties(*name, *update);
+ } else if (*method == SubscribeMsg::kMethodStr) {
+ // subuid
+ int64_t subuid;
+ if (!ObjGetNumber(*params, "subuid", &error, &subuid)) {
+ goto err;
+ }
+
+ // options
+ PubSubOptionsImpl options;
+ auto optionsIt = params->find("options");
+ if (optionsIt != params->end()) {
+ auto joptions = optionsIt->second.get_ptr<wpi::json::object_t*>();
+ if (!joptions) {
+ error = "options must be an object";
+ goto err;
+ }
+
+ // periodic
+ auto periodicIt = joptions->find("periodic");
+ if (periodicIt != joptions->end()) {
+ double val;
+ if (!GetNumber(periodicIt->second, &val)) {
+ error = "periodic value must be a number";
+ goto err;
+ }
+ options.periodic = val;
+ options.periodicMs = val * 1000;
+ }
+
+ // send all changes
+ auto sendAllIt = joptions->find("all");
+ if (sendAllIt != joptions->end()) {
+ auto sendAll = sendAllIt->second.get_ptr<bool*>();
+ if (!sendAll) {
+ error = "all value must be a boolean";
+ goto err;
+ }
+ options.sendAll = *sendAll;
+ }
+
+ // topics only
+ auto topicsOnlyIt = joptions->find("topicsonly");
+ if (topicsOnlyIt != joptions->end()) {
+ auto topicsOnly = topicsOnlyIt->second.get_ptr<bool*>();
+ if (!topicsOnly) {
+ error = "topicsonly value must be a boolean";
+ goto err;
+ }
+ options.topicsOnly = *topicsOnly;
+ }
+
+ // prefix match
+ auto prefixMatchIt = joptions->find("prefix");
+ if (prefixMatchIt != joptions->end()) {
+ auto prefixMatch = prefixMatchIt->second.get_ptr<bool*>();
+ if (!prefixMatch) {
+ error = "prefix value must be a boolean";
+ goto err;
+ }
+ options.prefixMatch = *prefixMatch;
+ }
+ }
+
+ // topic names
+ std::vector<std::string> topicNames;
+ if (!ObjGetStringArray(*params, "topics", &error, &topicNames)) {
+ goto err;
+ }
+
+ // complete
+ out.ClientSubscribe(subuid, topicNames, options);
+ } else if (*method == UnsubscribeMsg::kMethodStr) {
+ // subuid
+ int64_t subuid;
+ if (!ObjGetNumber(*params, "subuid", &error, &subuid)) {
+ goto err;
+ }
+
+ // complete
+ out.ClientUnsubscribe(subuid);
+ } else {
+ error = fmt::format("unrecognized method '{}'", *method);
+ goto err;
+ }
+ } else if constexpr (std::is_same_v<T, ServerMessageHandler>) {
+ if (*method == AnnounceMsg::kMethodStr) {
+ // name
+ auto name = ObjGetString(*params, "name", &error);
+ if (!name) {
+ goto err;
+ }
+
+ // id
+ int64_t id;
+ if (!ObjGetNumber(*params, "id", &error, &id)) {
+ goto err;
+ }
+
+ // type
+ auto typeStr = ObjGetString(*params, "type", &error);
+ if (!typeStr) {
+ goto err;
+ }
+
+ // pubuid
+ std::optional<int64_t> pubuid;
+ auto pubuidIt = params->find("pubuid");
+ if (pubuidIt != params->end()) {
+ int64_t val;
+ if (!GetNumber(pubuidIt->second, &val)) {
+ error = "pubuid value must be a number";
+ goto err;
+ }
+ pubuid = val;
+ }
+
+ // properties
+ auto propertiesIt = params->find("properties");
+ if (propertiesIt == params->end()) {
+ error = "no properties key";
+ goto err;
+ }
+ auto properties = &propertiesIt->second;
+ if (!properties->is_object()) {
+ WPI_WARNING(logger, "{}: properties is not an object", *name);
+ *properties = wpi::json::object();
+ }
+
+ // complete
+ out.ServerAnnounce(*name, id, *typeStr, *properties, pubuid);
+ } else if (*method == UnannounceMsg::kMethodStr) {
+ // name
+ auto name = ObjGetString(*params, "name", &error);
+ if (!name) {
+ goto err;
+ }
+
+ // id
+ int64_t id;
+ if (!ObjGetNumber(*params, "id", &error, &id)) {
+ goto err;
+ }
+
+ // complete
+ out.ServerUnannounce(*name, id);
+ } else if (*method == PropertiesUpdateMsg::kMethodStr) {
+ // name
+ auto name = ObjGetString(*params, "name", &error);
+ if (!name) {
+ goto err;
+ }
+
+ // update
+ auto updateIt = params->find("update");
+ if (updateIt == params->end()) {
+ error = "no update key";
+ goto err;
+ }
+ auto update = &updateIt->second;
+ if (!update->is_object()) {
+ error = "update must be an object";
+ goto err;
+ }
+
+ bool ack = false;
+ auto ackIt = params->find("ack");
+ if (ackIt != params->end()) {
+ auto val = ackIt->second.get_ptr<bool*>();
+ if (!val) {
+ error = "ack must be a boolean";
+ goto err;
+ }
+ ack = *val;
+ }
+
+ // complete
+ out.ServerPropertiesUpdate(*name, *update, ack);
+ } else {
+ error = fmt::format("unrecognized method '{}'", *method);
+ goto err;
+ }
+ }
+ continue;
+ }
+ err:
+ WPI_WARNING(logger, "{}: {}", i, error);
+ }
+}
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+void nt::net::WireDecodeText(std::string_view in, ClientMessageHandler& out,
+ wpi::Logger& logger) {
+ ::WireDecodeTextImpl(in, out, logger);
+}
+
+void nt::net::WireDecodeText(std::string_view in, ServerMessageHandler& out,
+ wpi::Logger& logger) {
+ ::WireDecodeTextImpl(in, out, logger);
+}
+
+bool nt::net::WireDecodeBinary(std::span<const uint8_t>* in, int64_t* outId,
+ Value* outValue, std::string* error,
+ int64_t localTimeOffset) {
+ mpack_reader_t reader;
+ mpack_reader_init_data(&reader, reinterpret_cast<const char*>(in->data()),
+ in->size());
+ mpack_expect_array_match(&reader, 4);
+ *outId = mpack_expect_i64(&reader);
+ auto time = mpack_expect_i64(&reader);
+ int type = mpack_expect_int(&reader);
+ switch (type) {
+ case 0: // boolean
+ *outValue = Value::MakeBoolean(mpack_expect_bool(&reader), 1);
+ break;
+ case 2: // integer
+ *outValue = Value::MakeInteger(mpack_expect_i64(&reader), 1);
+ break;
+ case 3: // float
+ *outValue = Value::MakeFloat(mpack_expect_float(&reader), 1);
+ break;
+ case 1: // double
+ *outValue = Value::MakeDouble(mpack_expect_double(&reader), 1);
+ break;
+ case 4: { // string
+ auto length = mpack_expect_str(&reader);
+ auto data = mpack_read_bytes_inplace(&reader, length);
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ *outValue = Value::MakeString({data, length}, 1);
+ }
+ mpack_done_str(&reader);
+ break;
+ }
+ case 5: { // raw
+ auto length = mpack_expect_bin(&reader);
+ auto data = mpack_read_bytes_inplace(&reader, length);
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ *outValue =
+ Value::MakeRaw({reinterpret_cast<const uint8_t*>(data), length}, 1);
+ }
+ mpack_done_bin(&reader);
+ break;
+ }
+ case 16: { // boolean array
+ auto length = mpack_expect_array(&reader);
+ std::vector<int> arr;
+ arr.reserve((std::min)(length, 1000u));
+ for (uint32_t i = 0; i < length; ++i) {
+ arr.emplace_back(mpack_expect_bool(&reader));
+ if (mpack_reader_error(&reader) != mpack_ok) {
+ break;
+ }
+ }
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ *outValue = Value::MakeBooleanArray(std::move(arr), 1);
+ }
+ mpack_done_array(&reader);
+ break;
+ }
+ case 18: { // integer array
+ auto length = mpack_expect_array(&reader);
+ std::vector<int64_t> arr;
+ arr.reserve((std::min)(length, 1000u));
+ for (uint32_t i = 0; i < length; ++i) {
+ arr.emplace_back(mpack_expect_i64(&reader));
+ if (mpack_reader_error(&reader) != mpack_ok) {
+ break;
+ }
+ }
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ *outValue = Value::MakeIntegerArray(std::move(arr), 1);
+ }
+ mpack_done_array(&reader);
+ break;
+ }
+ case 19: { // float array
+ auto length = mpack_expect_array(&reader);
+ std::vector<float> arr;
+ arr.reserve((std::min)(length, 1000u));
+ for (uint32_t i = 0; i < length; ++i) {
+ arr.emplace_back(mpack_expect_float(&reader));
+ if (mpack_reader_error(&reader) != mpack_ok) {
+ break;
+ }
+ }
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ *outValue = Value::MakeFloatArray(std::move(arr), 1);
+ }
+ mpack_done_array(&reader);
+ break;
+ }
+ case 17: { // double array
+ auto length = mpack_expect_array(&reader);
+ std::vector<double> arr;
+ arr.reserve((std::min)(length, 1000u));
+ for (uint32_t i = 0; i < length; ++i) {
+ arr.emplace_back(mpack_expect_double(&reader));
+ if (mpack_reader_error(&reader) != mpack_ok) {
+ break;
+ }
+ }
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ *outValue = Value::MakeDoubleArray(std::move(arr), 1);
+ }
+ mpack_done_array(&reader);
+ break;
+ }
+ case 20: { // string array
+ auto length = mpack_expect_array(&reader);
+ std::vector<std::string> arr;
+ arr.reserve((std::min)(length, 1000u));
+ for (uint32_t i = 0; i < length; ++i) {
+ auto length = mpack_expect_str(&reader);
+ auto data = mpack_read_bytes_inplace(&reader, length);
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ arr.emplace_back(std::string{data, length});
+ } else {
+ break;
+ }
+ mpack_done_str(&reader);
+ }
+ if (mpack_reader_error(&reader) == mpack_ok) {
+ *outValue = Value::MakeStringArray(std::move(arr), 1);
+ }
+ mpack_done_array(&reader);
+ break;
+ }
+ default:
+ *error = fmt::format("unrecognized type {}", type);
+ return false;
+ }
+ mpack_done_array(&reader);
+ auto err = mpack_reader_destroy(&reader);
+ if (err != mpack_ok) {
+ *error = mpack_error_to_string(err);
+ return false;
+ }
+ // set time
+ outValue->SetServerTime(time);
+ outValue->SetTime(time == 0 ? 0 : time + localTimeOffset);
+ // update input range
+ *in = wpi::drop_front(*in,
+ in->size() - mpack_reader_remaining(&reader, nullptr));
+ return true;
+}
diff --git a/ntcore/src/main/native/cpp/net/WireDecoder.h b/ntcore/src/main/native/cpp/net/WireDecoder.h
new file mode 100644
index 0000000..128dff2
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/WireDecoder.h
@@ -0,0 +1,64 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <stdint.h>
+
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+
+namespace wpi {
+class Logger;
+class json;
+} // namespace wpi
+
+namespace nt {
+class PubSubOptionsImpl;
+class Value;
+} // namespace nt
+
+namespace nt::net {
+
+class ClientMessageHandler {
+ public:
+ virtual ~ClientMessageHandler() = default;
+
+ virtual void ClientPublish(int64_t pubuid, std::string_view name,
+ std::string_view typeStr,
+ const wpi::json& properties) = 0;
+ virtual void ClientUnpublish(int64_t pubuid) = 0;
+ virtual void ClientSetProperties(std::string_view name,
+ const wpi::json& update) = 0;
+ virtual void ClientSubscribe(int64_t subuid,
+ std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options) = 0;
+ virtual void ClientUnsubscribe(int64_t subuid) = 0;
+};
+
+class ServerMessageHandler {
+ public:
+ virtual ~ServerMessageHandler() = default;
+ virtual void ServerAnnounce(std::string_view name, int64_t id,
+ std::string_view typeStr,
+ const wpi::json& properties,
+ std::optional<int64_t> pubuid) = 0;
+ virtual void ServerUnannounce(std::string_view name, int64_t id) = 0;
+ virtual void ServerPropertiesUpdate(std::string_view name,
+ const wpi::json& update, bool ack) = 0;
+};
+
+void WireDecodeText(std::string_view in, ClientMessageHandler& out,
+ wpi::Logger& logger);
+void WireDecodeText(std::string_view in, ServerMessageHandler& out,
+ wpi::Logger& logger);
+
+// returns true if successfully decoded a message
+bool WireDecodeBinary(std::span<const uint8_t>* in, int64_t* outId,
+ Value* outValue, std::string* error,
+ int64_t localTimeOffset);
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net/WireEncoder.cpp b/ntcore/src/main/native/cpp/net/WireEncoder.cpp
new file mode 100644
index 0000000..143fe0d
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/WireEncoder.cpp
@@ -0,0 +1,316 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "WireEncoder.h"
+
+#include <optional>
+
+#include <wpi/json_serializer.h>
+#include <wpi/mpack.h>
+#include <wpi/raw_ostream.h>
+
+#include "Handle.h"
+#include "Message.h"
+#include "PubSubOptions.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace nt;
+using namespace nt::net;
+using namespace mpack;
+
+void nt::net::WireEncodePublish(wpi::raw_ostream& os, int64_t pubuid,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << PublishMsg::kMethodStr << "\",\"params\":{";
+ os << "\"name\":\"";
+ s.dump_escaped(name, false);
+ os << "\",\"properties\":";
+ s.dump(properties, false, false, 0, 0);
+ os << ",\"pubuid\":";
+ s.dump_integer(pubuid);
+ os << ",\"type\":\"";
+ s.dump_escaped(typeStr, false);
+ os << "\"}}";
+}
+
+void nt::net::WireEncodeUnpublish(wpi::raw_ostream& os, int64_t pubuid) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << UnpublishMsg::kMethodStr << "\",\"params\":{";
+ os << "\"pubuid\":";
+ s.dump_integer(pubuid);
+ os << "}}";
+}
+
+void nt::net::WireEncodeSetProperties(wpi::raw_ostream& os,
+ std::string_view name,
+ const wpi::json& update) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << SetPropertiesMsg::kMethodStr << "\",\"params\":{";
+ os << "\"name\":\"";
+ s.dump_escaped(name, false);
+ os << "\",\"update\":";
+ s.dump(update, false, false, 0, 0);
+ os << "}}";
+}
+
+template <typename T>
+static void EncodePrefixes(wpi::raw_ostream& os, std::span<const T> topicNames,
+ wpi::json::serializer& s) {
+ os << '[';
+ bool first = true;
+ for (auto&& name : topicNames) {
+ if (first) {
+ first = false;
+ } else {
+ os << ',';
+ }
+ os << '"';
+ s.dump_escaped(name, false);
+ os << '"';
+ }
+ os << ']';
+}
+
+template <typename T>
+static void WireEncodeSubscribeImpl(wpi::raw_ostream& os, int64_t subuid,
+ std::span<const T> topicNames,
+ const PubSubOptionsImpl& options) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << SubscribeMsg::kMethodStr << "\",\"params\":{";
+ os << "\"options\":{";
+ bool first = true;
+ if (options.sendAll) {
+ os << "\"all\":true";
+ first = false;
+ }
+ if (options.topicsOnly) {
+ if (!first) {
+ os << ',';
+ }
+ os << "\"topicsonly\":true";
+ first = false;
+ }
+ if (options.prefixMatch) {
+ if (!first) {
+ os << ',';
+ }
+ os << "\"prefix\":true";
+ first = false;
+ }
+ if (options.periodicMs != PubSubOptionsImpl::kDefaultPeriodicMs) {
+ if (!first) {
+ os << ',';
+ }
+ os << "\"periodic\":";
+ s.dump_float(options.periodicMs / 1000.0);
+ }
+ os << "},\"topics\":";
+ EncodePrefixes(os, topicNames, s);
+ os << ",\"subuid\":";
+ s.dump_integer(subuid);
+ os << "}}";
+}
+
+void nt::net::WireEncodeSubscribe(wpi::raw_ostream& os, int64_t subuid,
+ std::span<const std::string_view> topicNames,
+ const PubSubOptionsImpl& options) {
+ WireEncodeSubscribeImpl(os, subuid, topicNames, options);
+}
+
+void nt::net::WireEncodeSubscribe(wpi::raw_ostream& os, int64_t subuid,
+ std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options) {
+ WireEncodeSubscribeImpl(os, subuid, topicNames, options);
+}
+
+void nt::net::WireEncodeUnsubscribe(wpi::raw_ostream& os, int64_t subHandle) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << UnsubscribeMsg::kMethodStr << "\",\"params\":{";
+ os << "\"subuid\":";
+ s.dump_integer(subHandle);
+ os << "}}";
+}
+
+bool nt::net::WireEncodeText(wpi::raw_ostream& os, const ClientMessage& msg) {
+ if (auto m = std::get_if<PublishMsg>(&msg.contents)) {
+ WireEncodePublish(os, Handle{m->pubHandle}.GetIndex(), m->name, m->typeStr,
+ m->properties);
+ } else if (auto m = std::get_if<UnpublishMsg>(&msg.contents)) {
+ WireEncodeUnpublish(os, Handle{m->pubHandle}.GetIndex());
+ } else if (auto m = std::get_if<SetPropertiesMsg>(&msg.contents)) {
+ WireEncodeSetProperties(os, m->name, m->update);
+ } else if (auto m = std::get_if<SubscribeMsg>(&msg.contents)) {
+ WireEncodeSubscribe(os, Handle{m->subHandle}.GetIndex(), m->topicNames,
+ m->options);
+ } else if (auto m = std::get_if<UnsubscribeMsg>(&msg.contents)) {
+ WireEncodeUnsubscribe(os, Handle{m->subHandle}.GetIndex());
+ } else {
+ return false;
+ }
+ return true;
+}
+
+void nt::net::WireEncodeAnnounce(wpi::raw_ostream& os, std::string_view name,
+ int64_t id, std::string_view typeStr,
+ const wpi::json& properties,
+ std::optional<int64_t> pubHandle) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << AnnounceMsg::kMethodStr << "\",\"params\":{";
+ os << "\"id\":";
+ s.dump_integer(id);
+ os << ",\"name\":\"";
+ s.dump_escaped(name, false);
+ os << "\",\"properties\":";
+ s.dump(properties, false, false, 0, 0);
+ if (pubHandle) {
+ os << ",\"pubuid\":";
+ s.dump_integer(*pubHandle);
+ }
+ os << ",\"type\":\"";
+ s.dump_escaped(typeStr, false);
+ os << "\"}}";
+}
+
+void nt::net::WireEncodeUnannounce(wpi::raw_ostream& os, std::string_view name,
+ int64_t id) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << UnannounceMsg::kMethodStr << "\",\"params\":{";
+ os << "\"id\":";
+ s.dump_integer(id);
+ os << ",\"name\":\"";
+ s.dump_escaped(name, false);
+ os << "\"}}";
+}
+
+void nt::net::WireEncodePropertiesUpdate(wpi::raw_ostream& os,
+ std::string_view name,
+ const wpi::json& update, bool ack) {
+ wpi::json::serializer s{os, ' ', 0};
+ os << "{\"method\":\"" << PropertiesUpdateMsg::kMethodStr
+ << "\",\"params\":{";
+ os << "\"name\":\"";
+ s.dump_escaped(name, false);
+ os << "\",\"update\":";
+ s.dump(update, false, false, 0, 0);
+ if (ack) {
+ os << ",\"ack\":true";
+ }
+ os << "}}";
+}
+
+bool nt::net::WireEncodeText(wpi::raw_ostream& os, const ServerMessage& msg) {
+ if (auto m = std::get_if<AnnounceMsg>(&msg.contents)) {
+ WireEncodeAnnounce(os, m->name, m->id, m->typeStr, m->properties,
+ m->pubuid);
+ } else if (auto m = std::get_if<UnannounceMsg>(&msg.contents)) {
+ WireEncodeUnannounce(os, m->name, m->id);
+ } else if (auto m = std::get_if<PropertiesUpdateMsg>(&msg.contents)) {
+ WireEncodePropertiesUpdate(os, m->name, m->update, m->ack);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+bool nt::net::WireEncodeBinary(wpi::raw_ostream& os, int64_t id, int64_t time,
+ const Value& value) {
+ char buf[128];
+ mpack_writer_t writer;
+ mpack_writer_init(&writer, buf, sizeof(buf));
+ mpack_writer_set_context(&writer, &os);
+ mpack_writer_set_flush(
+ &writer, [](mpack_writer_t* writer, const char* buffer, size_t count) {
+ static_cast<wpi::raw_ostream*>(writer->context)->write(buffer, count);
+ });
+ mpack_start_array(&writer, 4);
+ mpack_write_int(&writer, id);
+ mpack_write_int(&writer, time);
+ switch (value.type()) {
+ case NT_BOOLEAN:
+ mpack_write_u8(&writer, 0);
+ mpack_write_bool(&writer, value.GetBoolean());
+ break;
+ case NT_INTEGER:
+ mpack_write_u8(&writer, 2);
+ mpack_write_int(&writer, value.GetInteger());
+ break;
+ case NT_FLOAT:
+ mpack_write_u8(&writer, 3);
+ mpack_write_float(&writer, value.GetFloat());
+ break;
+ case NT_DOUBLE:
+ mpack_write_u8(&writer, 1);
+ mpack_write_double(&writer, value.GetDouble());
+ break;
+ case NT_STRING: {
+ auto v = value.GetString();
+ mpack_write_u8(&writer, 4);
+ mpack_write_str(&writer, v.data(), v.size());
+ break;
+ }
+ case NT_RPC:
+ case NT_RAW: {
+ auto v = value.GetRaw();
+ mpack_write_u8(&writer, 5);
+ mpack_write_bin(&writer, reinterpret_cast<const char*>(v.data()),
+ v.size());
+ break;
+ }
+ case NT_BOOLEAN_ARRAY: {
+ auto v = value.GetBooleanArray();
+ mpack_write_u8(&writer, 16);
+ mpack_start_array(&writer, v.size());
+ for (auto val : v) {
+ mpack_write_bool(&writer, val);
+ }
+ mpack_finish_array(&writer);
+ break;
+ }
+ case NT_INTEGER_ARRAY: {
+ auto v = value.GetIntegerArray();
+ mpack_write_u8(&writer, 18);
+ mpack_start_array(&writer, v.size());
+ for (auto val : v) {
+ mpack_write_int(&writer, val);
+ }
+ mpack_finish_array(&writer);
+ break;
+ }
+ case NT_FLOAT_ARRAY: {
+ auto v = value.GetFloatArray();
+ mpack_write_u8(&writer, 19);
+ mpack_start_array(&writer, v.size());
+ for (auto val : v) {
+ mpack_write_float(&writer, val);
+ }
+ mpack_finish_array(&writer);
+ break;
+ }
+ case NT_DOUBLE_ARRAY: {
+ auto v = value.GetDoubleArray();
+ mpack_write_u8(&writer, 17);
+ mpack_start_array(&writer, v.size());
+ for (auto val : v) {
+ mpack_write_double(&writer, val);
+ }
+ mpack_finish_array(&writer);
+ break;
+ }
+ case NT_STRING_ARRAY: {
+ auto v = value.GetStringArray();
+ mpack_write_u8(&writer, 20);
+ mpack_start_array(&writer, v.size());
+ for (auto&& val : v) {
+ mpack_write_str(&writer, val.data(), val.size());
+ }
+ mpack_finish_array(&writer);
+ break;
+ }
+ default:
+ return false;
+ }
+ mpack_finish_array(&writer);
+ return mpack_writer_destroy(&writer) == mpack_ok;
+}
diff --git a/ntcore/src/main/native/cpp/net/WireEncoder.h b/ntcore/src/main/native/cpp/net/WireEncoder.h
new file mode 100644
index 0000000..d0a04cb
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net/WireEncoder.h
@@ -0,0 +1,61 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+
+namespace wpi {
+class json;
+class raw_ostream;
+} // namespace wpi
+
+namespace nt {
+class PubSubOptionsImpl;
+class Value;
+} // namespace nt
+
+namespace nt::net {
+
+struct ClientMessage;
+struct ServerMessage;
+
+// encoders for client text messages (avoids need to construct a Message struct)
+void WireEncodePublish(wpi::raw_ostream& os, int64_t pubuid,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties);
+void WireEncodeUnpublish(wpi::raw_ostream& os, int64_t pubuid);
+void WireEncodeSetProperties(wpi::raw_ostream& os, std::string_view name,
+ const wpi::json& update);
+void WireEncodeSubscribe(wpi::raw_ostream& os, int64_t subuid,
+ std::span<const std::string_view> topicNames,
+ const PubSubOptionsImpl& options);
+void WireEncodeSubscribe(wpi::raw_ostream& os, int64_t subuid,
+ std::span<const std::string> topicNames,
+ const PubSubOptionsImpl& options);
+void WireEncodeUnsubscribe(wpi::raw_ostream& os, int64_t subuid);
+
+// encoders for server text messages (avoids need to construct a Message struct)
+void WireEncodeAnnounce(wpi::raw_ostream& os, std::string_view name, int64_t id,
+ std::string_view typeStr, const wpi::json& properties,
+ std::optional<int64_t> pubuid);
+void WireEncodeUnannounce(wpi::raw_ostream& os, std::string_view name,
+ int64_t id);
+void WireEncodePropertiesUpdate(wpi::raw_ostream& os, std::string_view name,
+ const wpi::json& update, bool ack);
+
+// Encode a single message; note text messages must be put into a
+// JSON array "[msg1, msg2]" for transmission.
+// Returns true if message was written
+bool WireEncodeText(wpi::raw_ostream& os, const ClientMessage& msg);
+bool WireEncodeText(wpi::raw_ostream& os, const ServerMessage& msg);
+
+// encoder for binary messages
+bool WireEncodeBinary(wpi::raw_ostream& os, int64_t id, int64_t time,
+ const Value& value);
+
+} // namespace nt::net
diff --git a/ntcore/src/main/native/cpp/net3/ClientImpl3.cpp b/ntcore/src/main/native/cpp/net3/ClientImpl3.cpp
new file mode 100644
index 0000000..0783865
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/ClientImpl3.cpp
@@ -0,0 +1,642 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "ClientImpl3.h"
+
+#include <algorithm>
+#include <numeric>
+#include <string>
+#include <vector>
+
+#include <fmt/format.h>
+#include <wpi/DenseMap.h>
+#include <wpi/StringMap.h>
+#include <wpi/json.h>
+
+#include "Handle.h"
+#include "Log.h"
+#include "Types_internal.h"
+#include "net/Message.h"
+#include "net/NetworkInterface.h"
+#include "net3/Message3.h"
+#include "net3/SequenceNumber.h"
+#include "net3/WireConnection3.h"
+#include "net3/WireDecoder3.h"
+#include "net3/WireEncoder3.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace nt;
+using namespace nt::net3;
+
+static constexpr uint32_t kMinPeriodMs = 5;
+
+// maximum number of times the wire can be not ready to send another
+// transmission before we close the connection
+static constexpr int kWireMaxNotReady = 10;
+
+namespace {
+
+struct Entry;
+
+struct PublisherData {
+ explicit PublisherData(Entry* entry) : entry{entry} {}
+
+ Entry* entry;
+ NT_Publisher handle;
+ PubSubOptionsImpl options;
+ // in options as double, but copy here as integer; rounded to the nearest
+ // 10 ms
+ uint32_t periodMs;
+ uint64_t nextSendMs{0};
+ std::vector<Value> outValues; // outgoing values
+};
+
+// data for each entry
+struct Entry {
+ explicit Entry(std::string_view name_) : name(name_) {}
+ bool IsPersistent() const { return (flags & NT_PERSISTENT) != 0; }
+ wpi::json SetFlags(unsigned int flags_);
+
+ std::string name;
+
+ std::string typeStr;
+ NT_Type type{NT_UNASSIGNED};
+
+ wpi::json properties = wpi::json::object();
+
+ // The current value and flags
+ Value value;
+ unsigned int flags{0};
+
+ // Unique ID used in network messages; this is 0xffff until assigned
+ // by the server.
+ unsigned int id{0xffff};
+
+ // Sequence number for update resolution
+ SequenceNumber seqNum;
+
+ // Local topic handle
+ NT_Topic topic{0};
+
+ // Local publishers
+ std::vector<PublisherData*> publishers;
+};
+
+class CImpl : public MessageHandler3 {
+ public:
+ CImpl(uint64_t curTimeMs, int inst, WireConnection3& wire,
+ wpi::Logger& logger,
+ std::function<void(uint32_t repeatMs)> setPeriodic);
+
+ void ProcessIncoming(std::span<const uint8_t> data);
+ void HandleLocal(std::span<const net::ClientMessage> msgs);
+ void SendPeriodic(uint64_t curTimeMs, bool initial);
+ void SendValue(Writer& out, Entry* entry, const Value& value);
+ bool CheckNetworkReady();
+
+ // Outgoing handlers
+ void Publish(NT_Publisher pubHandle, NT_Topic topicHandle,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties, const PubSubOptionsImpl& options);
+ void Unpublish(NT_Publisher pubHandle, NT_Topic topicHandle);
+ void SetProperties(NT_Topic topicHandle, std::string_view name,
+ const wpi::json& update);
+ void SetValue(NT_Publisher pubHandle, const Value& value);
+
+ // MessageHandler interface
+ void KeepAlive() final;
+ void ServerHelloDone() final;
+ void ClientHelloDone() final;
+ void ClearEntries() final;
+ void ProtoUnsup(unsigned int proto_rev) final;
+ void ClientHello(std::string_view self_id, unsigned int proto_rev) final;
+ void ServerHello(unsigned int flags, std::string_view self_id) final;
+ void EntryAssign(std::string_view name, unsigned int id, unsigned int seq_num,
+ const Value& value, unsigned int flags) final;
+ void EntryUpdate(unsigned int id, unsigned int seq_num,
+ const Value& value) final;
+ void FlagsUpdate(unsigned int id, unsigned int flags) final;
+ void EntryDelete(unsigned int id) final;
+ void ExecuteRpc(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> params) final {}
+ void RpcResponse(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> result) final {}
+
+ enum State {
+ kStateInitial,
+ kStateHelloSent,
+ kStateInitialAssignments,
+ kStateRunning
+ };
+
+ int m_inst;
+ WireConnection3& m_wire;
+ wpi::Logger& m_logger;
+ net::LocalInterface* m_local{nullptr};
+ std::function<void(uint32_t repeatMs)> m_setPeriodic;
+ uint64_t m_initTimeMs;
+
+ // periodic sweep handling
+ static constexpr uint32_t kKeepAliveIntervalMs = 1000;
+ uint32_t m_periodMs{kKeepAliveIntervalMs + 10};
+ uint64_t m_lastSendMs{0};
+ uint64_t m_nextKeepAliveTimeMs;
+ int m_notReadyCount{0};
+
+ // indexed by publisher index
+ std::vector<std::unique_ptr<PublisherData>> m_publishers;
+
+ State m_state{kStateInitial};
+ WireDecoder3 m_decoder;
+ std::string m_remoteId;
+ std::function<void()> m_handshakeSucceeded;
+
+ std::vector<std::pair<unsigned int, unsigned int>> m_outgoingFlags;
+
+ using NameMap = wpi::StringMap<std::unique_ptr<Entry>>;
+ using IdMap = std::vector<Entry*>;
+
+ NameMap m_nameMap;
+ IdMap m_idMap;
+
+ Entry* GetOrNewEntry(std::string_view name) {
+ auto& entry = m_nameMap[name];
+ if (!entry) {
+ entry = std::make_unique<Entry>(name);
+ }
+ return entry.get();
+ }
+ Entry* LookupId(unsigned int id) {
+ return id < m_idMap.size() ? m_idMap[id] : nullptr;
+ }
+};
+
+} // namespace
+
+wpi::json Entry::SetFlags(unsigned int flags_) {
+ bool wasPersistent = IsPersistent();
+ flags = flags_;
+ bool isPersistent = IsPersistent();
+ if (isPersistent && !wasPersistent) {
+ properties["persistent"] = true;
+ return {{"persistent", true}};
+ } else if (!isPersistent && wasPersistent) {
+ properties.erase("persistent");
+ return {{"persistent", wpi::json()}};
+ } else {
+ return wpi::json::object();
+ }
+}
+
+CImpl::CImpl(uint64_t curTimeMs, int inst, WireConnection3& wire,
+ wpi::Logger& logger,
+ std::function<void(uint32_t repeatMs)> setPeriodic)
+ : m_inst{inst},
+ m_wire{wire},
+ m_logger{logger},
+ m_setPeriodic{std::move(setPeriodic)},
+ m_initTimeMs{curTimeMs},
+ m_nextKeepAliveTimeMs{curTimeMs + kKeepAliveIntervalMs},
+ m_decoder{*this} {}
+
+void CImpl::ProcessIncoming(std::span<const uint8_t> data) {
+ DEBUG4("received {} bytes", data.size());
+ if (!m_decoder.Execute(&data)) {
+ m_wire.Disconnect(m_decoder.GetError());
+ }
+}
+
+void CImpl::HandleLocal(std::span<const net::ClientMessage> msgs) {
+ for (const auto& elem : msgs) { // NOLINT
+ // common case is value
+ if (auto msg = std::get_if<net::ClientValueMsg>(&elem.contents)) {
+ SetValue(msg->pubHandle, msg->value);
+ } else if (auto msg = std::get_if<net::PublishMsg>(&elem.contents)) {
+ Publish(msg->pubHandle, msg->topicHandle, msg->name, msg->typeStr,
+ msg->properties, msg->options);
+ } else if (auto msg = std::get_if<net::UnpublishMsg>(&elem.contents)) {
+ Unpublish(msg->pubHandle, msg->topicHandle);
+ } else if (auto msg = std::get_if<net::SetPropertiesMsg>(&elem.contents)) {
+ SetProperties(msg->topicHandle, msg->name, msg->update);
+ }
+ }
+}
+
+void CImpl::SendPeriodic(uint64_t curTimeMs, bool initial) {
+ DEBUG4("SendPeriodic({})", curTimeMs);
+
+ // rate limit sends
+ if (curTimeMs < (m_lastSendMs + kMinPeriodMs)) {
+ return;
+ }
+
+ auto out = m_wire.Send();
+
+ // send keep-alives
+ if (curTimeMs >= m_nextKeepAliveTimeMs) {
+ if (!CheckNetworkReady()) {
+ return;
+ }
+ DEBUG4("Sending keep alive");
+ WireEncodeKeepAlive(out.stream());
+ // drift isn't critical here, so just go from current time
+ m_nextKeepAliveTimeMs = curTimeMs + kKeepAliveIntervalMs;
+ }
+
+ // send any stored-up flags updates
+ if (!m_outgoingFlags.empty()) {
+ if (!CheckNetworkReady()) {
+ return;
+ }
+ for (auto&& p : m_outgoingFlags) {
+ WireEncodeFlagsUpdate(out.stream(), p.first, p.second);
+ }
+ m_outgoingFlags.resize(0);
+ }
+
+ // send any pending updates due to be sent
+ bool checkedNetwork = false;
+ for (auto&& pub : m_publishers) {
+ if (pub && !pub->outValues.empty() && curTimeMs >= pub->nextSendMs) {
+ if (!checkedNetwork) {
+ if (!CheckNetworkReady()) {
+ return;
+ }
+ checkedNetwork = true;
+ }
+ for (auto&& val : pub->outValues) {
+ SendValue(out, pub->entry, val);
+ }
+ pub->outValues.resize(0);
+ pub->nextSendMs = curTimeMs + pub->periodMs;
+ }
+ }
+
+ if (initial) {
+ DEBUG4("Sending ClientHelloDone");
+ WireEncodeClientHelloDone(out.stream());
+ }
+
+ m_wire.Flush();
+ m_lastSendMs = curTimeMs;
+}
+
+void CImpl::SendValue(Writer& out, Entry* entry, const Value& value) {
+ DEBUG4("sending value for '{}', seqnum {}", entry->name,
+ entry->seqNum.value());
+
+ // bump sequence number
+ ++entry->seqNum;
+
+ // only send assigns during initial handshake
+ if (entry->id == 0xffff || m_state == kStateInitialAssignments) {
+ // send assign
+ WireEncodeEntryAssign(out.stream(), entry->name, entry->id,
+ entry->seqNum.value(), value, entry->flags);
+ } else {
+ // send update
+ WireEncodeEntryUpdate(out.stream(), entry->id, entry->seqNum.value(),
+ value);
+ }
+}
+
+bool CImpl::CheckNetworkReady() {
+ if (!m_wire.Ready()) {
+ ++m_notReadyCount;
+ if (m_notReadyCount > kWireMaxNotReady) {
+ m_wire.Disconnect("transmit stalled");
+ }
+ return false;
+ }
+ m_notReadyCount = 0;
+ return true;
+}
+
+void CImpl::Publish(NT_Publisher pubHandle, NT_Topic topicHandle,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptionsImpl& options) {
+ DEBUG4("Publish('{}', '{}')", name, typeStr);
+ unsigned int index = Handle{pubHandle}.GetIndex();
+ if (index >= m_publishers.size()) {
+ m_publishers.resize(index + 1);
+ }
+ auto& publisher = m_publishers[index];
+ if (!publisher) {
+ publisher = std::make_unique<PublisherData>(GetOrNewEntry(name));
+ publisher->entry->typeStr = typeStr;
+ publisher->entry->type = StringToType3(typeStr);
+ publisher->entry->publishers.emplace_back(publisher.get());
+ }
+ publisher->handle = pubHandle;
+ publisher->options = options;
+ publisher->periodMs = std::lround(options.periodicMs / 10.0) * 10;
+ if (publisher->periodMs < 10) {
+ publisher->periodMs = 10;
+ }
+
+ // update period
+ m_periodMs = std::gcd(m_periodMs, publisher->periodMs);
+ m_setPeriodic(m_periodMs);
+}
+
+void CImpl::Unpublish(NT_Publisher pubHandle, NT_Topic topicHandle) {
+ DEBUG4("Unpublish({}, {})", pubHandle, topicHandle);
+ unsigned int index = Handle{pubHandle}.GetIndex();
+ if (index >= m_publishers.size()) {
+ return;
+ }
+ auto& publisher = m_publishers[index];
+ publisher->entry->publishers.erase(
+ std::remove(publisher->entry->publishers.begin(),
+ publisher->entry->publishers.end(), publisher.get()),
+ publisher->entry->publishers.end());
+ publisher.reset();
+
+ // loop over all publishers to update period
+ m_periodMs = kKeepAliveIntervalMs + 10;
+ for (auto&& pub : m_publishers) {
+ if (pub) {
+ m_periodMs = std::gcd(m_periodMs, pub->periodMs);
+ }
+ }
+ m_setPeriodic(m_periodMs);
+}
+
+void CImpl::SetProperties(NT_Topic topicHandle, std::string_view name,
+ const wpi::json& update) {
+ DEBUG4("SetProperties({}, {}, {})", topicHandle, name, update.dump());
+ auto entry = GetOrNewEntry(name);
+ bool updated = false;
+ for (auto&& elem : update.items()) {
+ entry->properties[elem.key()] = elem.value();
+ if (elem.key() == "persistent") {
+ if (auto val = elem.value().get_ptr<const bool*>()) {
+ if (*val) {
+ entry->flags |= NT_PERSISTENT;
+ } else {
+ entry->flags &= ~NT_PERSISTENT;
+ }
+ updated = true;
+ }
+ }
+ }
+ if (updated && entry->id == 0xffff) {
+ m_outgoingFlags.emplace_back(entry->id, entry->flags);
+ }
+}
+
+void CImpl::SetValue(NT_Publisher pubHandle, const Value& value) {
+ DEBUG4("SetValue({})", pubHandle);
+ unsigned int index = Handle{pubHandle}.GetIndex();
+ assert(index < m_publishers.size() && m_publishers[index]);
+ auto& publisher = *m_publishers[index];
+ if (value == publisher.entry->value) {
+ return;
+ }
+ publisher.entry->value = value;
+ if (publisher.outValues.empty() || publisher.options.sendAll) {
+ publisher.outValues.emplace_back(value);
+ } else {
+ publisher.outValues.back() = value;
+ }
+}
+
+void CImpl::KeepAlive() {
+ DEBUG4("KeepAlive()");
+ if (m_state != kStateRunning && m_state != kStateInitialAssignments) {
+ m_decoder.SetError("received unexpected KeepAlive message");
+ return;
+ }
+ // ignore
+}
+
+void CImpl::ServerHelloDone() {
+ DEBUG4("ServerHelloDone()");
+ if (m_state != kStateInitialAssignments) {
+ m_decoder.SetError("received unexpected ServerHelloDone message");
+ return;
+ }
+
+ // send initial assignments
+ SendPeriodic(m_initTimeMs, true);
+
+ m_state = kStateRunning;
+ m_setPeriodic(m_periodMs);
+}
+
+void CImpl::ClientHelloDone() {
+ DEBUG4("ClientHelloDone()");
+ m_decoder.SetError("received unexpected ClientHelloDone message");
+}
+
+void CImpl::ProtoUnsup(unsigned int proto_rev) {
+ DEBUG4("ProtoUnsup({})", proto_rev);
+ m_decoder.SetError(fmt::format("received ProtoUnsup(version={})", proto_rev));
+}
+
+void CImpl::ClientHello(std::string_view self_id, unsigned int proto_rev) {
+ DEBUG4("ClientHello({}, {})", self_id, proto_rev);
+ m_decoder.SetError("received unexpected ClientHello message");
+}
+
+void CImpl::ServerHello(unsigned int flags, std::string_view self_id) {
+ DEBUG4("ServerHello({}, {})", flags, self_id);
+ if (m_state != kStateHelloSent) {
+ m_decoder.SetError("received unexpected ServerHello message");
+ return;
+ }
+ m_state = kStateInitialAssignments;
+ m_remoteId = self_id;
+ m_handshakeSucceeded();
+ m_handshakeSucceeded = nullptr; // no longer required
+}
+
+void CImpl::EntryAssign(std::string_view name, unsigned int id,
+ unsigned int seq_num, const Value& value,
+ unsigned int flags) {
+ DEBUG4("EntryAssign({}, {}, {}, value, {})", name, id, seq_num, flags);
+ if (m_state != kStateInitialAssignments && m_state != kStateRunning) {
+ m_decoder.SetError("received unexpected EntryAssign message");
+ return;
+ }
+ auto entry = GetOrNewEntry(name);
+ bool flagsChanged = entry->flags != flags;
+ bool typeChanged;
+ bool valueChanged;
+
+ // don't update value if we locally published a "strong" value
+ if (m_state == kStateInitialAssignments && entry->value &&
+ entry->value.server_time() != 0) {
+ typeChanged = false;
+ valueChanged = false;
+ } else {
+ typeChanged = entry->type != value.type();
+ valueChanged = entry->value != value;
+ if (m_state == kStateInitialAssignments) {
+ // remove outgoing during initial assignments so we don't get out of sync
+ for (auto publisher : entry->publishers) {
+ publisher->outValues.clear();
+ }
+ }
+ }
+
+ entry->id = id;
+ entry->seqNum = SequenceNumber{seq_num};
+ entry->SetFlags(flags);
+ if (typeChanged) {
+ entry->type = value.type();
+ entry->typeStr = TypeToString(value.type());
+ }
+ if (valueChanged) {
+ entry->value = value;
+ }
+
+ // add to id map
+ if (id >= m_idMap.size()) {
+ m_idMap.resize(id + 1);
+ }
+ m_idMap[id] = entry;
+
+ if (m_local) {
+ // XXX: need to handle type change specially? (e.g. with unannounce)
+ if (entry->topic == 0 || flagsChanged || typeChanged) {
+ DEBUG4("NetworkAnnounce({}, {})", name, entry->typeStr);
+ entry->topic =
+ m_local->NetworkAnnounce(name, entry->typeStr, entry->properties, 0);
+ }
+ if (valueChanged) {
+ m_local->NetworkSetValue(entry->topic, entry->value);
+ }
+ }
+}
+
+void CImpl::EntryUpdate(unsigned int id, unsigned int seq_num,
+ const Value& value) {
+ DEBUG4("EntryUpdate({}, {}, value)", id, seq_num);
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received EntryUpdate message before ServerHelloDone");
+ return;
+ }
+ if (auto entry = LookupId(id)) {
+ entry->value = value;
+ if (m_local && entry->topic != 0) {
+ m_local->NetworkSetValue(entry->topic, entry->value);
+ }
+ }
+}
+
+void CImpl::FlagsUpdate(unsigned int id, unsigned int flags) {
+ DEBUG4("FlagsUpdate({}, {})", id, flags);
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received FlagsUpdate message before ServerHelloDone");
+ return;
+ }
+ if (auto entry = LookupId(id)) {
+ wpi::json update = entry->SetFlags(flags);
+ if (!update.empty() && m_local) {
+ m_local->NetworkPropertiesUpdate(entry->name, update, false);
+ }
+ }
+
+ // erase any outgoing flags updates
+ m_outgoingFlags.erase(
+ std::remove_if(m_outgoingFlags.begin(), m_outgoingFlags.end(),
+ [&](const auto& p) { return p.first == id; }),
+ m_outgoingFlags.end());
+}
+
+void CImpl::EntryDelete(unsigned int id) {
+ DEBUG4("EntryDelete({})", id);
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received EntryDelete message before ServerHelloDone");
+ return;
+ }
+ if (auto entry = LookupId(id)) {
+ m_idMap[id] = nullptr;
+ // set id to 0xffff so any future local setvalue will result in assign
+ entry->id = 0xffff;
+ entry->value = Value{};
+
+ // if we have no local publishers, unannounce
+ if (entry->publishers.empty() && m_local) {
+ m_local->NetworkUnannounce(entry->name);
+ }
+ }
+
+ // erase any outgoing flags updates
+ m_outgoingFlags.erase(
+ std::remove_if(m_outgoingFlags.begin(), m_outgoingFlags.end(),
+ [&](const auto& p) { return p.first == id; }),
+ m_outgoingFlags.end());
+}
+
+void CImpl::ClearEntries() {
+ DEBUG4("ClearEntries()");
+ if (m_state != kStateRunning) {
+ m_decoder.SetError("received ClearEntries message before ServerHelloDone");
+ return;
+ }
+ for (auto& entry : m_idMap) {
+ if (entry && entry->id != 0xffff && !entry->IsPersistent()) {
+ entry->id = 0xffff;
+ entry->value = Value{};
+
+ // if we have no local publishers, unannounce
+ if (entry->publishers.empty() && m_local) {
+ m_local->NetworkUnannounce(entry->name);
+ }
+
+ entry = nullptr; // clear id mapping
+ }
+ }
+
+ // erase all outgoing flags updates
+ m_outgoingFlags.resize(0);
+}
+
+class ClientImpl3::Impl final : public CImpl {
+ public:
+ Impl(uint64_t curTimeMs, int inst, WireConnection3& wire, wpi::Logger& logger,
+ std::function<void(uint32_t repeatMs)> setPeriodic)
+ : CImpl{curTimeMs, inst, wire, logger, std::move(setPeriodic)} {}
+};
+
+ClientImpl3::ClientImpl3(uint64_t curTimeMs, int inst, WireConnection3& wire,
+ wpi::Logger& logger,
+ std::function<void(uint32_t repeatMs)> setPeriodic)
+ : m_impl{std::make_unique<Impl>(curTimeMs, inst, wire, logger,
+ std::move(setPeriodic))} {}
+
+ClientImpl3::~ClientImpl3() {
+ WPI_DEBUG4(m_impl->m_logger, "NT3 ClientImpl destroyed");
+}
+
+void ClientImpl3::Start(std::string_view selfId,
+ std::function<void()> succeeded) {
+ if (m_impl->m_state != CImpl::kStateInitial) {
+ return;
+ }
+ m_impl->m_handshakeSucceeded = std::move(succeeded);
+ auto writer = m_impl->m_wire.Send();
+ WireEncodeClientHello(writer.stream(), selfId, 0x0300);
+ m_impl->m_wire.Flush();
+ m_impl->m_state = CImpl::kStateHelloSent;
+}
+
+void ClientImpl3::ProcessIncoming(std::span<const uint8_t> data) {
+ m_impl->ProcessIncoming(data);
+}
+
+void ClientImpl3::HandleLocal(std::span<const net::ClientMessage> msgs) {
+ m_impl->HandleLocal(msgs);
+}
+
+void ClientImpl3::SendPeriodic(uint64_t curTimeMs) {
+ m_impl->SendPeriodic(curTimeMs, false);
+}
+
+void ClientImpl3::SetLocal(net::LocalInterface* local) {
+ m_impl->m_local = local;
+}
diff --git a/ntcore/src/main/native/cpp/net3/ClientImpl3.h b/ntcore/src/main/native/cpp/net3/ClientImpl3.h
new file mode 100644
index 0000000..484ea3d
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/ClientImpl3.h
@@ -0,0 +1,50 @@
+// 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 <functional>
+#include <memory>
+#include <span>
+#include <string>
+#include <string_view>
+
+#include "net/NetworkInterface.h"
+
+namespace wpi {
+class Logger;
+} // namespace wpi
+
+namespace nt::net {
+struct ClientMessage;
+class LocalInterface;
+} // namespace nt::net
+
+namespace nt::net3 {
+
+class WireConnection3;
+
+class ClientImpl3 {
+ public:
+ explicit ClientImpl3(uint64_t curTimeMs, int inst, WireConnection3& wire,
+ wpi::Logger& logger,
+ std::function<void(uint32_t repeatMs)> setPeriodic);
+ ~ClientImpl3();
+
+ void Start(std::string_view selfId, std::function<void()> succeeded);
+ void ProcessIncoming(std::span<const uint8_t> data);
+ void HandleLocal(std::span<const net::ClientMessage> msgs);
+
+ void SendPeriodic(uint64_t curTimeMs);
+
+ void SetLocal(net::LocalInterface* local);
+
+ private:
+ class Impl;
+ std::unique_ptr<Impl> m_impl;
+};
+
+} // namespace nt::net3
diff --git a/ntcore/src/main/native/cpp/net3/Message3.h b/ntcore/src/main/native/cpp/net3/Message3.h
new file mode 100644
index 0000000..ebac75f
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/Message3.h
@@ -0,0 +1,156 @@
+// 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 <span>
+#include <string>
+
+#include "networktables/NetworkTableValue.h"
+#include "ntcore_c.h"
+
+namespace nt::net3 {
+
+class WireDecoder3;
+
+class Message3 {
+ struct private_init {};
+ friend class WireDecoder3;
+
+ public:
+ enum MsgType {
+ kUnknown = -1,
+ kKeepAlive = 0x00,
+ kClientHello = 0x01,
+ kProtoUnsup = 0x02,
+ kServerHelloDone = 0x03,
+ kServerHello = 0x04,
+ kClientHelloDone = 0x05,
+ kEntryAssign = 0x10,
+ kEntryUpdate = 0x11,
+ kFlagsUpdate = 0x12,
+ kEntryDelete = 0x13,
+ kClearEntries = 0x14,
+ kExecuteRpc = 0x20,
+ kRpcResponse = 0x21
+ };
+ enum DataType {
+ kBoolean = 0x00,
+ kDouble = 0x01,
+ kString = 0x02,
+ kRaw = 0x03,
+ kBooleanArray = 0x10,
+ kDoubleArray = 0x11,
+ kStringArray = 0x12,
+ kRpcDef = 0x20
+ };
+ static constexpr uint32_t kClearAllMagic = 0xD06CB27Aul;
+
+ Message3() = default;
+ Message3(MsgType type, const private_init&) : m_type(type) {}
+
+ MsgType type() const { return m_type; }
+ bool Is(MsgType type) const { return type == m_type; }
+
+ // Message data accessors. Callers are responsible for knowing what data is
+ // actually provided for a particular message.
+ std::string_view str() const { return m_str; }
+ std::span<const uint8_t> bytes() const {
+ return {reinterpret_cast<const uint8_t*>(m_str.data()), m_str.size()};
+ }
+ const Value& value() const { return m_value; }
+ unsigned int id() const { return m_id; }
+ unsigned int flags() const { return m_flags; }
+ unsigned int seq_num_uid() const { return m_seq_num_uid; }
+
+ void SetValue(const Value& value) { m_value = value; }
+
+ // Create messages without data
+ static Message3 KeepAlive() { return {kKeepAlive, {}}; }
+ static Message3 ServerHelloDone() { return {kServerHelloDone, {}}; }
+ static Message3 ClientHelloDone() { return {kClientHelloDone, {}}; }
+ static Message3 ClearEntries() { return {kClearEntries, {}}; }
+
+ // Create messages with data
+ static Message3 ProtoUnsup(unsigned int proto_rev = 0x0300u) {
+ Message3 msg{kProtoUnsup, {}};
+ msg.m_id = proto_rev;
+ return msg;
+ }
+ static Message3 ClientHello(std::string_view self_id,
+ unsigned int proto_rev = 0x0300u) {
+ Message3 msg{kClientHello, {}};
+ msg.m_str = self_id;
+ msg.m_id = proto_rev;
+ return msg;
+ }
+ static Message3 ServerHello(unsigned int flags, std::string_view self_id) {
+ Message3 msg{kServerHello, {}};
+ msg.m_str = self_id;
+ msg.m_flags = flags;
+ return msg;
+ }
+ static Message3 EntryAssign(std::string_view name, unsigned int id,
+ unsigned int seq_num, const Value& value,
+ unsigned int flags) {
+ Message3 msg{kEntryAssign, {}};
+ msg.m_str = name;
+ msg.m_value = value;
+ msg.m_id = id;
+ msg.m_flags = flags;
+ msg.m_seq_num_uid = seq_num;
+ return msg;
+ }
+ static Message3 EntryUpdate(unsigned int id, unsigned int seq_num,
+ const Value& value) {
+ Message3 msg{kEntryUpdate, {}};
+ msg.m_value = value;
+ msg.m_id = id;
+ msg.m_seq_num_uid = seq_num;
+ return msg;
+ }
+ static Message3 FlagsUpdate(unsigned int id, unsigned int flags) {
+ Message3 msg{kFlagsUpdate, {}};
+ msg.m_id = id;
+ msg.m_flags = flags;
+ return msg;
+ }
+ static Message3 EntryDelete(unsigned int id) {
+ Message3 msg{kEntryDelete, {}};
+ msg.m_id = id;
+ return msg;
+ }
+ static Message3 ExecuteRpc(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> params) {
+ Message3 msg{kExecuteRpc, {}};
+ msg.m_str.assign(reinterpret_cast<const char*>(params.data()),
+ params.size());
+ msg.m_id = id;
+ msg.m_seq_num_uid = uid;
+ return msg;
+ }
+ static Message3 RpcResponse(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> result) {
+ Message3 msg{kRpcResponse, {}};
+ msg.m_str.assign(reinterpret_cast<const char*>(result.data()),
+ result.size());
+ msg.m_id = id;
+ msg.m_seq_num_uid = uid;
+ return msg;
+ }
+
+ private:
+ MsgType m_type{kUnknown};
+
+ // Message data. Use varies by message type.
+ std::string m_str;
+ Value m_value;
+ unsigned int m_id{0}; // also used for proto_rev
+ unsigned int m_flags{0};
+ unsigned int m_seq_num_uid{0};
+};
+
+} // namespace nt::net3
diff --git a/ntcore/src/main/native/cpp/net3/SequenceNumber.h b/ntcore/src/main/native/cpp/net3/SequenceNumber.h
new file mode 100644
index 0000000..d8bf995
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/SequenceNumber.h
@@ -0,0 +1,38 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <compare>
+
+namespace nt::net3 {
+
+/* A sequence number per RFC 1982 */
+class SequenceNumber {
+ public:
+ SequenceNumber() = default;
+ explicit SequenceNumber(unsigned int value) : m_value(value) {}
+ unsigned int value() const { return m_value; }
+
+ SequenceNumber& operator++() {
+ ++m_value;
+ if (m_value > 0xffff) {
+ m_value = 0;
+ }
+ return *this;
+ }
+ SequenceNumber operator++(int) {
+ SequenceNumber tmp(*this);
+ operator++();
+ return tmp;
+ }
+
+ friend auto operator<=>(const SequenceNumber& lhs,
+ const SequenceNumber& rhs) = default;
+
+ private:
+ unsigned int m_value{0};
+};
+
+} // namespace nt::net3
diff --git a/ntcore/src/main/native/cpp/net3/UvStreamConnection3.cpp b/ntcore/src/main/native/cpp/net3/UvStreamConnection3.cpp
new file mode 100644
index 0000000..93af700
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/UvStreamConnection3.cpp
@@ -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.
+
+#include "UvStreamConnection3.h"
+
+#include <wpinet/uv/Stream.h>
+
+using namespace nt;
+using namespace nt::net3;
+
+UvStreamConnection3::UvStreamConnection3(wpi::uv::Stream& stream)
+ : m_stream{stream}, m_os{m_buffers, [this] { return AllocBuf(); }} {}
+
+UvStreamConnection3::~UvStreamConnection3() {
+ for (auto&& buf : m_buf_pool) {
+ buf.Deallocate();
+ }
+}
+
+void UvStreamConnection3::Flush() {
+ if (m_buffers.empty()) {
+ return;
+ }
+ ++m_sendsActive;
+ m_stream.Write(m_buffers, [selfweak = weak_from_this()](auto bufs, auto) {
+ if (auto self = selfweak.lock()) {
+ self->m_buf_pool.insert(self->m_buf_pool.end(), bufs.begin(), bufs.end());
+ if (self->m_sendsActive > 0) {
+ --self->m_sendsActive;
+ }
+ }
+ });
+ m_buffers.clear();
+ m_os.reset();
+}
+
+void UvStreamConnection3::Disconnect(std::string_view reason) {
+ m_reason = reason;
+ m_stream.Close();
+}
+
+void UvStreamConnection3::FinishSend() {}
+
+wpi::uv::Buffer UvStreamConnection3::AllocBuf() {
+ if (!m_buf_pool.empty()) {
+ auto buf = m_buf_pool.back();
+ m_buf_pool.pop_back();
+ return buf;
+ }
+ return wpi::uv::Buffer::Allocate(kAllocSize);
+}
diff --git a/ntcore/src/main/native/cpp/net3/UvStreamConnection3.h b/ntcore/src/main/native/cpp/net3/UvStreamConnection3.h
new file mode 100644
index 0000000..f94c4a3
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/UvStreamConnection3.h
@@ -0,0 +1,60 @@
+// 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 <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <wpi/SmallVector.h>
+#include <wpinet/raw_uv_ostream.h>
+#include <wpinet/uv/Buffer.h>
+
+#include "net3/WireConnection3.h"
+
+namespace wpi::uv {
+class Stream;
+} // namespace wpi::uv
+
+namespace nt::net3 {
+
+class UvStreamConnection3 final
+ : public WireConnection3,
+ public std::enable_shared_from_this<UvStreamConnection3> {
+ static constexpr size_t kAllocSize = 4096;
+
+ public:
+ explicit UvStreamConnection3(wpi::uv::Stream& stream);
+ ~UvStreamConnection3() override;
+ UvStreamConnection3(const UvStreamConnection3&) = delete;
+ UvStreamConnection3& operator=(const UvStreamConnection3&) = delete;
+
+ bool Ready() const final { return m_sendsActive == 0; }
+
+ Writer Send() final { return {m_os, *this}; }
+
+ void Flush() final;
+
+ void Disconnect(std::string_view reason) final;
+
+ std::string_view GetDisconnectReason() const { return m_reason; }
+
+ wpi::uv::Stream& GetStream() { return m_stream; }
+
+ private:
+ void FinishSend() final;
+
+ wpi::uv::Buffer AllocBuf();
+
+ wpi::uv::Stream& m_stream;
+ wpi::SmallVector<wpi::uv::Buffer, 4> m_buffers;
+ std::vector<wpi::uv::Buffer> m_buf_pool;
+ wpi::raw_uv_ostream m_os;
+ std::string m_reason;
+ int m_sendsActive = 0;
+};
+
+} // namespace nt::net3
diff --git a/ntcore/src/main/native/cpp/net3/WireConnection3.h b/ntcore/src/main/native/cpp/net3/WireConnection3.h
new file mode 100644
index 0000000..85453d7
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/WireConnection3.h
@@ -0,0 +1,65 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <string_view>
+
+namespace wpi {
+class raw_ostream;
+} // namespace wpi
+
+namespace nt::net3 {
+
+class Writer;
+
+class WireConnection3 {
+ friend class Writer;
+
+ public:
+ virtual ~WireConnection3() = default;
+
+ virtual bool Ready() const = 0;
+
+ virtual Writer Send() = 0;
+
+ virtual void Flush() = 0;
+
+ virtual void Disconnect(std::string_view reason) = 0;
+
+ protected:
+ virtual void FinishSend() = 0;
+};
+
+class Writer {
+ public:
+ Writer(wpi::raw_ostream& os, WireConnection3& wire)
+ : m_os{&os}, m_wire{&wire} {}
+ Writer(const Writer&) = delete;
+ Writer(Writer&& rhs) : m_os{rhs.m_os}, m_wire{rhs.m_wire} {
+ rhs.m_os = nullptr;
+ rhs.m_wire = nullptr;
+ }
+ ~Writer() {
+ if (m_wire) {
+ m_wire->FinishSend();
+ }
+ }
+ Writer& operator=(const Writer&) = delete;
+ Writer& operator=(Writer&& rhs) {
+ m_os = rhs.m_os;
+ m_wire = rhs.m_wire;
+ rhs.m_os = nullptr;
+ rhs.m_wire = nullptr;
+ return *this;
+ }
+
+ wpi::raw_ostream& stream() { return *m_os; }
+
+ private:
+ wpi::raw_ostream* m_os;
+ WireConnection3* m_wire;
+};
+
+} // namespace nt::net3
diff --git a/ntcore/src/main/native/cpp/net3/WireDecoder3.cpp b/ntcore/src/main/native/cpp/net3/WireDecoder3.cpp
new file mode 100644
index 0000000..ea08358
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/WireDecoder3.cpp
@@ -0,0 +1,600 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "WireDecoder3.h"
+
+#include <algorithm>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include <fmt/format.h>
+#include <wpi/MathExtras.h>
+#include <wpi/SpanExtras.h>
+#include <wpi/leb128.h>
+
+#include "Message3.h"
+
+using namespace nt;
+using namespace nt::net3;
+
+namespace {
+
+class SimpleValueReader {
+ public:
+ std::optional<uint16_t> Read16(std::span<const uint8_t>* in);
+ std::optional<uint32_t> Read32(std::span<const uint8_t>* in);
+ std::optional<uint64_t> Read64(std::span<const uint8_t>* in);
+ std::optional<double> ReadDouble(std::span<const uint8_t>* in);
+
+ private:
+ uint64_t m_value = 0;
+ int m_count = 0;
+};
+
+struct StringReader {
+ void SetLen(uint64_t len_) {
+ len = len_;
+ buf.clear();
+ }
+
+ std::optional<uint64_t> len;
+ std::string buf;
+};
+
+struct RawReader {
+ void SetLen(uint64_t len_) {
+ len = len_;
+ buf.clear();
+ }
+
+ std::optional<uint64_t> len;
+ std::vector<uint8_t> buf;
+};
+
+struct ValueReader {
+ ValueReader() = default;
+ explicit ValueReader(NT_Type type_) : type{type_} {}
+
+ void SetSize(uint32_t size_) {
+ haveSize = true;
+ size = size_;
+ ints.clear();
+ doubles.clear();
+ strings.clear();
+ }
+
+ NT_Type type = NT_UNASSIGNED;
+ bool haveSize = false;
+ uint32_t size = 0;
+ std::vector<int> ints;
+ std::vector<double> doubles;
+ std::vector<std::string> strings;
+};
+
+struct WDImpl {
+ explicit WDImpl(MessageHandler3& out) : m_out{out} {}
+
+ MessageHandler3& m_out;
+
+ // primary (message) decode state
+ enum {
+ kStart,
+ kClientHello_1ProtoRev,
+ kClientHello_2Id,
+ kProtoUnsup_1ProtoRev,
+ kServerHello_1Flags,
+ kServerHello_2Id,
+ kEntryAssign_1Name,
+ kEntryAssign_2Type,
+ kEntryAssign_3Id,
+ kEntryAssign_4SeqNum,
+ kEntryAssign_5Flags,
+ kEntryAssign_6Value,
+ kEntryUpdate_1Id,
+ kEntryUpdate_2SeqNum,
+ kEntryUpdate_3Type,
+ kEntryUpdate_4Value,
+ kFlagsUpdate_1Id,
+ kFlagsUpdate_2Flags,
+ kEntryDelete_1Id,
+ kClearEntries_1Magic,
+ kExecuteRpc_1Id,
+ kExecuteRpc_2Uid,
+ kExecuteRpc_3Params,
+ kRpcResponse_1Id,
+ kRpcResponse_2Uid,
+ kRpcResponse_3Result,
+ kError
+ } m_state = kStart;
+
+ // detail decoders
+ wpi::Uleb128Reader m_ulebReader;
+ SimpleValueReader m_simpleReader;
+ StringReader m_stringReader;
+ RawReader m_rawReader;
+ ValueReader m_valueReader;
+
+ std::string m_error;
+
+ std::string m_str;
+ unsigned int m_id{0}; // also used for proto_rev
+ unsigned int m_flags{0};
+ unsigned int m_seq_num_uid{0};
+
+ void Execute(std::span<const uint8_t>* in);
+
+ std::nullopt_t EmitError(std::string_view msg) {
+ m_state = kError;
+ m_error = msg;
+ return std::nullopt;
+ }
+
+ std::optional<std::string> ReadString(std::span<const uint8_t>* in);
+ std::optional<std::vector<uint8_t>> ReadRaw(std::span<const uint8_t>* in);
+ std::optional<NT_Type> ReadType(std::span<const uint8_t>* in);
+ std::optional<Value> ReadValue(std::span<const uint8_t>* in);
+};
+
+} // namespace
+
+static uint8_t Read8(std::span<const uint8_t>* in) {
+ uint8_t val = in->front();
+ *in = wpi::drop_front(*in);
+ return val;
+}
+
+std::optional<uint16_t> SimpleValueReader::Read16(
+ std::span<const uint8_t>* in) {
+ while (!in->empty()) {
+ m_value <<= 8;
+ m_value |= in->front() & 0xff;
+ *in = wpi::drop_front(*in);
+ if (++m_count >= 2) {
+ uint16_t val = static_cast<uint16_t>(m_value);
+ m_count = 0;
+ m_value = 0;
+ return val;
+ }
+ }
+ return std::nullopt;
+}
+
+std::optional<uint32_t> SimpleValueReader::Read32(
+ std::span<const uint8_t>* in) {
+ while (!in->empty()) {
+ m_value <<= 8;
+ m_value |= in->front() & 0xff;
+ *in = wpi::drop_front(*in);
+ if (++m_count >= 4) {
+ uint32_t val = static_cast<uint32_t>(m_value);
+ m_count = 0;
+ m_value = 0;
+ return val;
+ }
+ }
+ return std::nullopt;
+}
+
+std::optional<uint64_t> SimpleValueReader::Read64(
+ std::span<const uint8_t>* in) {
+ while (!in->empty()) {
+ m_value <<= 8;
+ m_value |= in->front() & 0xff;
+ *in = wpi::drop_front(*in);
+ if (++m_count >= 8) {
+ uint64_t val = m_value;
+ m_count = 0;
+ m_value = 0;
+ return val;
+ }
+ }
+ return std::nullopt;
+}
+
+std::optional<double> SimpleValueReader::ReadDouble(
+ std::span<const uint8_t>* in) {
+ if (auto val = Read64(in)) {
+ return wpi::BitsToDouble(val.value());
+ } else {
+ return std::nullopt;
+ }
+}
+
+void WDImpl::Execute(std::span<const uint8_t>* in) {
+ while (!in->empty()) {
+ switch (m_state) {
+ case kStart: {
+ uint8_t msgType = Read8(in);
+ switch (msgType) {
+ case Message3::kKeepAlive:
+ m_out.KeepAlive();
+ break;
+ case Message3::kClientHello:
+ m_state = kClientHello_1ProtoRev;
+ break;
+ case Message3::kProtoUnsup:
+ m_state = kProtoUnsup_1ProtoRev;
+ break;
+ case Message3::kServerHello:
+ m_state = kServerHello_1Flags;
+ break;
+ case Message3::kServerHelloDone:
+ m_out.ServerHelloDone();
+ break;
+ case Message3::kClientHelloDone:
+ m_out.ClientHelloDone();
+ break;
+ case Message3::kEntryAssign:
+ m_state = kEntryAssign_1Name;
+ break;
+ case Message3::kEntryUpdate:
+ m_state = kEntryUpdate_1Id;
+ break;
+ case Message3::kFlagsUpdate:
+ m_state = kFlagsUpdate_1Id;
+ break;
+ case Message3::kEntryDelete:
+ m_state = kEntryDelete_1Id;
+ break;
+ case Message3::kClearEntries:
+ m_state = kClearEntries_1Magic;
+ break;
+ case Message3::kExecuteRpc:
+ m_state = kExecuteRpc_1Id;
+ break;
+ case Message3::kRpcResponse:
+ m_state = kRpcResponse_1Id;
+ break;
+ default:
+ EmitError(fmt::format("unrecognized message type: {}",
+ static_cast<uint32_t>(msgType)));
+ return;
+ }
+ break;
+ }
+ case kClientHello_1ProtoRev:
+ if (auto val = m_simpleReader.Read16(in)) {
+ if (val < 0x0300u) {
+ m_state = kStart;
+ m_out.ClientHello("", val.value());
+ } else {
+ m_state = kClientHello_2Id;
+ m_id = val.value();
+ }
+ }
+ break;
+ case kClientHello_2Id:
+ if (auto val = ReadString(in)) {
+ m_state = kStart;
+ m_out.ClientHello(val.value(), m_id);
+ }
+ break;
+ case kProtoUnsup_1ProtoRev:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kStart;
+ m_out.ProtoUnsup(val.value());
+ }
+ break;
+ case kServerHello_1Flags: {
+ m_state = kServerHello_2Id;
+ m_flags = Read8(in);
+ break;
+ }
+ case kServerHello_2Id:
+ if (auto val = ReadString(in)) {
+ m_state = kStart;
+ m_out.ServerHello(m_flags, val.value());
+ }
+ break;
+ case kEntryAssign_1Name:
+ if (auto val = ReadString(in)) {
+ m_state = kEntryAssign_2Type;
+ m_str = std::move(val.value());
+ }
+ break;
+ case kEntryAssign_2Type:
+ if (auto val = ReadType(in)) {
+ m_state = kEntryAssign_3Id;
+ m_valueReader = ValueReader{val.value()};
+ }
+ break;
+ case kEntryAssign_3Id:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kEntryAssign_4SeqNum;
+ m_id = val.value();
+ }
+ break;
+ case kEntryAssign_4SeqNum:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kEntryAssign_5Flags;
+ m_seq_num_uid = val.value();
+ }
+ break;
+ case kEntryAssign_5Flags: {
+ m_state = kEntryAssign_6Value;
+ m_flags = Read8(in);
+ break;
+ }
+ case kEntryAssign_6Value:
+ if (auto val = ReadValue(in)) {
+ m_state = kStart;
+ m_out.EntryAssign(m_str, m_id, m_seq_num_uid, val.value(), m_flags);
+ }
+ break;
+ case kEntryUpdate_1Id:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kEntryUpdate_2SeqNum;
+ m_id = val.value();
+ }
+ break;
+ case kEntryUpdate_2SeqNum:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kEntryUpdate_3Type;
+ m_seq_num_uid = val.value();
+ }
+ break;
+ case kEntryUpdate_3Type:
+ if (auto val = ReadType(in)) {
+ m_state = kEntryUpdate_4Value;
+ m_valueReader = ValueReader{val.value()};
+ }
+ break;
+ case kEntryUpdate_4Value:
+ if (auto val = ReadValue(in)) {
+ m_state = kStart;
+ m_out.EntryUpdate(m_id, m_seq_num_uid, val.value());
+ }
+ break;
+ case kFlagsUpdate_1Id:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kFlagsUpdate_2Flags;
+ m_id = val.value();
+ }
+ break;
+ case kFlagsUpdate_2Flags: {
+ m_state = kStart;
+ m_out.FlagsUpdate(m_id, Read8(in));
+ break;
+ }
+ case kEntryDelete_1Id:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kStart;
+ m_out.EntryDelete(val.value());
+ }
+ break;
+ case kClearEntries_1Magic:
+ if (auto val = m_simpleReader.Read32(in)) {
+ m_state = kStart;
+ if (val.value() == Message3::kClearAllMagic) {
+ m_out.ClearEntries();
+ } else {
+ EmitError("received incorrect CLEAR_ENTRIES magic value");
+ }
+ break;
+ }
+ break;
+ case kExecuteRpc_1Id:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kExecuteRpc_2Uid;
+ m_id = val.value();
+ }
+ break;
+ case kExecuteRpc_2Uid:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kExecuteRpc_3Params;
+ m_seq_num_uid = val.value();
+ }
+ break;
+ case kExecuteRpc_3Params:
+ if (auto val = ReadRaw(in)) {
+ m_state = kStart;
+ m_out.ExecuteRpc(m_id, m_seq_num_uid, val.value());
+ }
+ break;
+ case kRpcResponse_1Id:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kRpcResponse_2Uid;
+ m_id = val.value();
+ }
+ break;
+ case kRpcResponse_2Uid:
+ if (auto val = m_simpleReader.Read16(in)) {
+ m_state = kRpcResponse_3Result;
+ m_seq_num_uid = val.value();
+ }
+ break;
+ case kRpcResponse_3Result:
+ if (auto val = ReadRaw(in)) {
+ m_state = kStart;
+ m_out.RpcResponse(m_id, m_seq_num_uid, val.value());
+ }
+ break;
+ case kError:
+ return;
+ }
+ }
+}
+
+std::optional<std::string> WDImpl::ReadString(std::span<const uint8_t>* in) {
+ // string length
+ if (!m_stringReader.len) {
+ if (auto val = m_ulebReader.ReadOne(in)) {
+ m_stringReader.SetLen(val.value());
+ m_stringReader.buf.clear();
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ // string data; nolint to avoid clang-tidy false positive
+ size_t toCopy =
+ (std::min)(in->size(),
+ static_cast<size_t>(m_stringReader.len.value() -
+ m_stringReader.buf.size())); // NOLINT
+ m_stringReader.buf.append(reinterpret_cast<const char*>(in->data()), toCopy);
+ *in = wpi::drop_front(*in, toCopy);
+ if (m_stringReader.buf.size() >= m_stringReader.len) {
+ m_stringReader.len.reset();
+ return std::move(m_stringReader.buf);
+ }
+ return std::nullopt;
+}
+
+std::optional<std::vector<uint8_t>> WDImpl::ReadRaw(
+ std::span<const uint8_t>* in) {
+ // string length
+ if (!m_rawReader.len) {
+ if (auto val = m_ulebReader.ReadOne(in)) {
+ m_rawReader.SetLen(val.value());
+ m_rawReader.buf.clear();
+ } else {
+ return std::nullopt;
+ }
+ }
+
+ // string data
+ size_t toCopy = (std::min)(
+ static_cast<size_t>(in->size()),
+ static_cast<size_t>(m_rawReader.len.value() - m_rawReader.buf.size()));
+ m_rawReader.buf.insert(m_rawReader.buf.end(), in->begin(),
+ in->begin() + toCopy);
+ *in = wpi::drop_front(*in, toCopy);
+ if (m_rawReader.buf.size() >= m_rawReader.len) {
+ m_rawReader.len.reset();
+ return std::move(m_rawReader.buf);
+ }
+ return std::nullopt;
+}
+
+std::optional<NT_Type> WDImpl::ReadType(std::span<const uint8_t>* in) {
+ // Convert from byte value to enum
+ switch (Read8(in)) {
+ case Message3::kBoolean:
+ return NT_BOOLEAN;
+ case Message3::kDouble:
+ return NT_DOUBLE;
+ case Message3::kString:
+ return NT_STRING;
+ case Message3::kRaw:
+ return NT_RAW;
+ case Message3::kBooleanArray:
+ return NT_BOOLEAN_ARRAY;
+ case Message3::kDoubleArray:
+ return NT_DOUBLE_ARRAY;
+ case Message3::kStringArray:
+ return NT_STRING_ARRAY;
+ case Message3::kRpcDef:
+ return NT_RPC;
+ default:
+ return EmitError("unrecognized value type");
+ }
+}
+
+std::optional<Value> WDImpl::ReadValue(std::span<const uint8_t>* in) {
+ while (!in->empty()) {
+ switch (m_valueReader.type) {
+ case NT_BOOLEAN:
+ return Value::MakeBoolean(Read8(in) != 0);
+ case NT_DOUBLE:
+ if (auto val = m_simpleReader.ReadDouble(in)) {
+ return Value::MakeDouble(val.value());
+ }
+ break;
+ case NT_STRING:
+ if (auto val = ReadString(in)) {
+ return Value::MakeString(std::move(val.value()));
+ }
+ break;
+ case NT_RAW:
+ case NT_RPC:
+ if (auto val = ReadRaw(in)) {
+ return Value::MakeRaw(std::move(val.value()));
+ }
+ break;
+#if 0
+ case NT_RPC:
+ if (auto val = ReadRaw(in)) {
+ return Value::MakeRpc(std::move(val.value()));
+ }
+ break;
+#endif
+ case NT_BOOLEAN_ARRAY:
+ // size
+ if (!m_valueReader.haveSize) {
+ m_valueReader.SetSize(Read8(in));
+ break;
+ }
+
+ // array values
+ while (!in->empty() && m_valueReader.ints.size() < m_valueReader.size) {
+ m_valueReader.ints.emplace_back(Read8(in) ? 1 : 0);
+ }
+ if (m_valueReader.ints.size() == m_valueReader.size) {
+ return Value::MakeBooleanArray(std::move(m_valueReader.ints));
+ }
+ break;
+ case NT_DOUBLE_ARRAY:
+ // size
+ if (!m_valueReader.haveSize) {
+ m_valueReader.SetSize(Read8(in));
+ break;
+ }
+
+ // array values
+ while (!in->empty() &&
+ m_valueReader.doubles.size() < m_valueReader.size) {
+ if (auto val = m_simpleReader.ReadDouble(in)) {
+ m_valueReader.doubles.emplace_back(std::move(val.value()));
+ }
+ }
+ if (m_valueReader.doubles.size() == m_valueReader.size) {
+ return Value::MakeDoubleArray(std::move(m_valueReader.doubles));
+ }
+ break;
+ case NT_STRING_ARRAY:
+ // size
+ if (!m_valueReader.haveSize) {
+ m_valueReader.SetSize(Read8(in));
+ break;
+ }
+
+ // array values
+ while (!in->empty() &&
+ m_valueReader.strings.size() < m_valueReader.size) {
+ if (auto val = ReadString(in)) {
+ m_valueReader.strings.emplace_back(std::move(val.value()));
+ }
+ }
+ if (m_valueReader.strings.size() == m_valueReader.size) {
+ return Value::MakeStringArray(std::move(m_valueReader.strings));
+ }
+ break;
+ default:
+ return EmitError("invalid type when trying to read value");
+ }
+ }
+ return std::nullopt;
+}
+
+struct WireDecoder3::Impl : public WDImpl {
+ explicit Impl(MessageHandler3& out) : WDImpl{out} {}
+};
+
+WireDecoder3::WireDecoder3(MessageHandler3& out) : m_impl{new Impl{out}} {}
+
+WireDecoder3::~WireDecoder3() = default;
+
+bool WireDecoder3::Execute(std::span<const uint8_t>* in) {
+ m_impl->Execute(in);
+ return m_impl->m_state != Impl::kError;
+}
+
+void WireDecoder3::SetError(std::string_view message) {
+ m_impl->EmitError(message);
+}
+
+std::string WireDecoder3::GetError() const {
+ return m_impl->m_error;
+}
diff --git a/ntcore/src/main/native/cpp/net3/WireDecoder3.h b/ntcore/src/main/native/cpp/net3/WireDecoder3.h
new file mode 100644
index 0000000..e877833
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/WireDecoder3.h
@@ -0,0 +1,64 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <stdint.h>
+
+#include <memory>
+#include <span>
+#include <string>
+
+namespace nt {
+class Value;
+} // namespace nt
+
+namespace nt::net3 {
+
+class MessageHandler3 {
+ public:
+ virtual void KeepAlive() = 0;
+ virtual void ServerHelloDone() = 0;
+ virtual void ClientHelloDone() = 0;
+ virtual void ClearEntries() = 0;
+ virtual void ProtoUnsup(unsigned int proto_rev) = 0;
+ virtual void ClientHello(std::string_view self_id,
+ unsigned int proto_rev) = 0;
+ virtual void ServerHello(unsigned int flags, std::string_view self_id) = 0;
+ virtual void EntryAssign(std::string_view name, unsigned int id,
+ unsigned int seq_num, const Value& value,
+ unsigned int flags) = 0;
+ virtual void EntryUpdate(unsigned int id, unsigned int seq_num,
+ const Value& value) = 0;
+ virtual void FlagsUpdate(unsigned int id, unsigned int flags) = 0;
+ virtual void EntryDelete(unsigned int id) = 0;
+ virtual void ExecuteRpc(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> params) = 0;
+ virtual void RpcResponse(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> result) = 0;
+};
+
+/* Decodes NT3 protocol into native representation. */
+class WireDecoder3 {
+ public:
+ explicit WireDecoder3(MessageHandler3& out);
+ ~WireDecoder3();
+
+ /**
+ * Executes the decoder. All input data will be consumed unless an error
+ * occurs.
+ * @param in input data (updated during parse)
+ * @return false if error occurred
+ */
+ bool Execute(std::span<const uint8_t>* in);
+
+ void SetError(std::string_view message);
+ std::string GetError() const;
+
+ private:
+ struct Impl;
+ std::unique_ptr<Impl> m_impl;
+};
+
+} // namespace nt::net3
diff --git a/ntcore/src/main/native/cpp/net3/WireEncoder3.cpp b/ntcore/src/main/native/cpp/net3/WireEncoder3.cpp
new file mode 100644
index 0000000..6bf3435
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/WireEncoder3.cpp
@@ -0,0 +1,326 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "WireEncoder3.h"
+
+#include <wpi/MathExtras.h>
+#include <wpi/SmallVector.h>
+#include <wpi/leb128.h>
+#include <wpi/raw_ostream.h>
+
+#include "Message3.h"
+
+using namespace nt;
+using namespace nt::net3;
+
+static void Write8(wpi::raw_ostream& os, uint8_t val) {
+ os << val;
+}
+
+static void Write16(wpi::raw_ostream& os, uint16_t val) {
+ os << std::span<const uint8_t>{{static_cast<uint8_t>((val >> 8) & 0xff),
+ static_cast<uint8_t>(val & 0xff)}};
+}
+
+static void Write32(wpi::raw_ostream& os, uint32_t val) {
+ os << std::span<const uint8_t>{{static_cast<uint8_t>((val >> 24) & 0xff),
+ static_cast<uint8_t>((val >> 16) & 0xff),
+ static_cast<uint8_t>((val >> 8) & 0xff),
+ static_cast<uint8_t>(val & 0xff)}};
+}
+
+static void WriteDouble(wpi::raw_ostream& os, double val) {
+ // The highest performance way to do this, albeit non-portable.
+ uint64_t v = wpi::DoubleToBits(val);
+ os << std::span<const uint8_t>{{static_cast<uint8_t>((v >> 56) & 0xff),
+ static_cast<uint8_t>((v >> 48) & 0xff),
+ static_cast<uint8_t>((v >> 40) & 0xff),
+ static_cast<uint8_t>((v >> 32) & 0xff),
+ static_cast<uint8_t>((v >> 24) & 0xff),
+ static_cast<uint8_t>((v >> 16) & 0xff),
+ static_cast<uint8_t>((v >> 8) & 0xff),
+ static_cast<uint8_t>(v & 0xff)}};
+}
+
+static void WriteString(wpi::raw_ostream& os, std::string_view str) {
+ wpi::WriteUleb128(os, str.size());
+ os << str;
+}
+
+static void WriteRaw(wpi::raw_ostream& os, std::span<const uint8_t> str) {
+ wpi::WriteUleb128(os, str.size());
+ os << str;
+}
+
+static bool WriteType(wpi::raw_ostream& os, NT_Type type) {
+ char ch;
+ // Convert from enum to actual byte value.
+ switch (type) {
+ case NT_BOOLEAN:
+ ch = Message3::kBoolean;
+ break;
+ case NT_INTEGER:
+ case NT_FLOAT:
+ case NT_DOUBLE:
+ ch = Message3::kDouble;
+ break;
+ case NT_STRING:
+ ch = Message3::kString;
+ break;
+ case NT_RAW:
+ ch = Message3::kRaw;
+ break;
+ case NT_BOOLEAN_ARRAY:
+ ch = Message3::kBooleanArray;
+ break;
+ case NT_INTEGER_ARRAY:
+ case NT_FLOAT_ARRAY:
+ case NT_DOUBLE_ARRAY:
+ ch = Message3::kDoubleArray;
+ break;
+ case NT_STRING_ARRAY:
+ ch = Message3::kStringArray;
+ break;
+ case NT_RPC:
+ ch = Message3::kRpcDef;
+ break;
+ default:
+ return false;
+ }
+ os << ch;
+ return true;
+}
+
+static bool WriteValue(wpi::raw_ostream& os, const Value& value) {
+ switch (value.type()) {
+ case NT_BOOLEAN:
+ Write8(os, value.GetBoolean() ? 1 : 0);
+ break;
+ case NT_INTEGER:
+ WriteDouble(os, value.GetInteger());
+ break;
+ case NT_FLOAT:
+ WriteDouble(os, value.GetFloat());
+ break;
+ case NT_DOUBLE:
+ WriteDouble(os, value.GetDouble());
+ break;
+ case NT_STRING:
+ WriteString(os, value.GetString());
+ break;
+ case NT_RAW:
+ WriteRaw(os, value.GetRaw());
+ break;
+ case NT_RPC:
+ WriteRaw(os, value.GetRaw());
+ break;
+ case NT_BOOLEAN_ARRAY: {
+ auto v = value.GetBooleanArray();
+ size_t size = v.size();
+ if (size > 0xff) {
+ size = 0xff; // size is only 1 byte, truncate
+ }
+ Write8(os, size);
+
+ for (size_t i = 0; i < size; ++i) {
+ Write8(os, v[i] ? 1 : 0);
+ }
+ break;
+ }
+ case NT_INTEGER_ARRAY: {
+ auto v = value.GetIntegerArray();
+ size_t size = v.size();
+ if (size > 0xff) {
+ size = 0xff; // size is only 1 byte, truncate
+ }
+ Write8(os, size);
+
+ for (size_t i = 0; i < size; ++i) {
+ WriteDouble(os, v[i]);
+ }
+ break;
+ }
+ case NT_FLOAT_ARRAY: {
+ auto v = value.GetFloatArray();
+ size_t size = v.size();
+ if (size > 0xff) {
+ size = 0xff; // size is only 1 byte, truncate
+ }
+ Write8(os, size);
+
+ for (size_t i = 0; i < size; ++i) {
+ WriteDouble(os, v[i]);
+ }
+ break;
+ }
+ case NT_DOUBLE_ARRAY: {
+ auto v = value.GetDoubleArray();
+ size_t size = v.size();
+ if (size > 0xff) {
+ size = 0xff; // size is only 1 byte, truncate
+ }
+ Write8(os, size);
+
+ for (size_t i = 0; i < size; ++i) {
+ WriteDouble(os, v[i]);
+ }
+ break;
+ }
+ case NT_STRING_ARRAY: {
+ auto v = value.GetStringArray();
+ size_t size = v.size();
+ if (size > 0xff) {
+ size = 0xff; // size is only 1 byte, truncate
+ }
+ Write8(os, size);
+
+ for (size_t i = 0; i < size; ++i) {
+ WriteString(os, v[i]);
+ }
+ break;
+ }
+ default:
+ return false;
+ }
+ return true;
+}
+
+void nt::net3::WireEncodeKeepAlive(wpi::raw_ostream& os) {
+ Write8(os, Message3::kKeepAlive);
+}
+
+void nt::net3::WireEncodeServerHelloDone(wpi::raw_ostream& os) {
+ Write8(os, Message3::kServerHelloDone);
+}
+
+void nt::net3::WireEncodeClientHelloDone(wpi::raw_ostream& os) {
+ Write8(os, Message3::kClientHelloDone);
+}
+
+void nt::net3::WireEncodeClearEntries(wpi::raw_ostream& os) {
+ Write8(os, Message3::kClearEntries);
+ Write32(os, Message3::kClearAllMagic);
+}
+
+void nt::net3::WireEncodeProtoUnsup(wpi::raw_ostream& os,
+ unsigned int proto_rev) {
+ Write8(os, Message3::kProtoUnsup);
+ Write16(os, proto_rev);
+}
+
+void nt::net3::WireEncodeClientHello(wpi::raw_ostream& os,
+ std::string_view self_id,
+ unsigned int proto_rev) {
+ Write8(os, Message3::kClientHello);
+ Write16(os, proto_rev);
+ WriteString(os, self_id);
+}
+
+void nt::net3::WireEncodeServerHello(wpi::raw_ostream& os, unsigned int flags,
+ std::string_view self_id) {
+ Write8(os, Message3::kServerHello);
+ Write8(os, flags);
+ WriteString(os, self_id);
+}
+
+bool nt::net3::WireEncodeEntryAssign(wpi::raw_ostream& os,
+ std::string_view name, unsigned int id,
+ unsigned int seq_num, const Value& value,
+ unsigned int flags) {
+ Write8(os, Message3::kEntryAssign);
+ WriteString(os, name);
+ WriteType(os, value.type());
+ Write16(os, id);
+ Write16(os, seq_num);
+ Write8(os, flags);
+ return WriteValue(os, value);
+}
+
+bool nt::net3::WireEncodeEntryUpdate(wpi::raw_ostream& os, unsigned int id,
+ unsigned int seq_num, const Value& value) {
+ Write8(os, Message3::kEntryUpdate);
+ Write16(os, id);
+ Write16(os, seq_num);
+ WriteType(os, value.type());
+ return WriteValue(os, value);
+}
+
+void nt::net3::WireEncodeFlagsUpdate(wpi::raw_ostream& os, unsigned int id,
+ unsigned int flags) {
+ Write8(os, Message3::kFlagsUpdate);
+ Write16(os, id);
+ Write8(os, flags);
+}
+
+void nt::net3::WireEncodeEntryDelete(wpi::raw_ostream& os, unsigned int id) {
+ Write8(os, Message3::kEntryDelete);
+ Write16(os, id);
+}
+
+void nt::net3::WireEncodeExecuteRpc(wpi::raw_ostream& os, unsigned int id,
+ unsigned int uid,
+ std::span<const uint8_t> params) {
+ Write8(os, Message3::kExecuteRpc);
+ Write16(os, id);
+ Write16(os, uid);
+ WriteRaw(os, params);
+}
+
+void nt::net3::WireEncodeRpcResponse(wpi::raw_ostream& os, unsigned int id,
+ unsigned int uid,
+ std::span<const uint8_t> result) {
+ Write8(os, Message3::kRpcResponse);
+ Write16(os, id);
+ Write16(os, uid);
+ WriteRaw(os, result);
+}
+
+bool nt::net3::WireEncode(wpi::raw_ostream& os, const Message3& msg) {
+ switch (msg.type()) {
+ case Message3::kKeepAlive:
+ WireEncodeKeepAlive(os);
+ break;
+ case Message3::kServerHelloDone:
+ WireEncodeServerHelloDone(os);
+ break;
+ case Message3::kClientHelloDone:
+ WireEncodeClientHelloDone(os);
+ break;
+ case Message3::kClientHello:
+ WireEncodeClientHello(os, msg.str(), msg.id());
+ break;
+ case Message3::kProtoUnsup:
+ WireEncodeProtoUnsup(os, msg.id());
+ break;
+ case Message3::kServerHello:
+ WireEncodeServerHello(os, msg.flags(), msg.str());
+ break;
+ case Message3::kEntryAssign:
+ return WireEncodeEntryAssign(os, msg.str(), msg.id(), msg.seq_num_uid(),
+ msg.value(), msg.flags());
+ case Message3::kEntryUpdate:
+ return WireEncodeEntryUpdate(os, msg.id(), msg.seq_num_uid(),
+ msg.value());
+ case Message3::kFlagsUpdate:
+ WireEncodeFlagsUpdate(os, msg.id(), msg.flags());
+ break;
+ case Message3::kEntryDelete:
+ WireEncodeEntryDelete(os, msg.id());
+ break;
+ case Message3::kClearEntries:
+ WireEncodeClearEntries(os);
+ break;
+ case Message3::kExecuteRpc:
+ WireEncodeExecuteRpc(os, msg.id(), msg.seq_num_uid(), msg.bytes());
+ break;
+ case Message3::kRpcResponse:
+ WireEncodeRpcResponse(os, msg.id(), msg.seq_num_uid(), msg.bytes());
+ break;
+ case Message3::kUnknown:
+ return true; // ignore
+ default:
+ return false;
+ }
+ return true;
+}
diff --git a/ntcore/src/main/native/cpp/net3/WireEncoder3.h b/ntcore/src/main/native/cpp/net3/WireEncoder3.h
new file mode 100644
index 0000000..66be9ae
--- /dev/null
+++ b/ntcore/src/main/native/cpp/net3/WireEncoder3.h
@@ -0,0 +1,49 @@
+// 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 <span>
+#include <string_view>
+
+namespace wpi {
+class raw_ostream;
+} // namespace wpi
+
+namespace nt {
+class Value;
+} // namespace nt
+
+namespace nt::net3 {
+
+class Message3;
+
+// encoders for messages (avoids need to construct a Message struct)
+void WireEncodeKeepAlive(wpi::raw_ostream& os);
+void WireEncodeServerHelloDone(wpi::raw_ostream& os);
+void WireEncodeClientHelloDone(wpi::raw_ostream& os);
+void WireEncodeClearEntries(wpi::raw_ostream& os);
+void WireEncodeProtoUnsup(wpi::raw_ostream& os, unsigned int proto_rev);
+void WireEncodeClientHello(wpi::raw_ostream& os, std::string_view self_id,
+ unsigned int proto_rev);
+void WireEncodeServerHello(wpi::raw_ostream& os, unsigned int flags,
+ std::string_view self_id);
+bool WireEncodeEntryAssign(wpi::raw_ostream& os, std::string_view name,
+ unsigned int id, unsigned int seq_num,
+ const Value& value, unsigned int flags);
+bool WireEncodeEntryUpdate(wpi::raw_ostream& os, unsigned int id,
+ unsigned int seq_num, const Value& value);
+void WireEncodeFlagsUpdate(wpi::raw_ostream& os, unsigned int id,
+ unsigned int flags);
+void WireEncodeEntryDelete(wpi::raw_ostream& os, unsigned int id);
+void WireEncodeExecuteRpc(wpi::raw_ostream& os, unsigned int id,
+ unsigned int uid, std::span<const uint8_t> params);
+void WireEncodeRpcResponse(wpi::raw_ostream& os, unsigned int id,
+ unsigned int uid, std::span<const uint8_t> result);
+
+bool WireEncode(wpi::raw_ostream& os, const Message3& msg);
+
+} // namespace nt::net3
diff --git a/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp b/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp
index 0c28dd4..1f6a760 100644
--- a/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp
+++ b/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp
@@ -12,8 +12,20 @@
#include <wpi/StringExtras.h>
#include <wpi/StringMap.h>
+#include "networktables/BooleanArrayTopic.h"
+#include "networktables/BooleanTopic.h"
+#include "networktables/DoubleArrayTopic.h"
+#include "networktables/DoubleTopic.h"
+#include "networktables/FloatArrayTopic.h"
+#include "networktables/FloatTopic.h"
+#include "networktables/IntegerArrayTopic.h"
+#include "networktables/IntegerTopic.h"
#include "networktables/NetworkTableInstance.h"
+#include "networktables/RawTopic.h"
+#include "networktables/StringArrayTopic.h"
+#include "networktables/StringTopic.h"
#include "ntcore.h"
+#include "ntcore_cpp.h"
using namespace nt;
@@ -78,11 +90,7 @@
const private_init&)
: m_inst(inst), m_path(path) {}
-NetworkTable::~NetworkTable() {
- for (auto i : m_listeners) {
- RemoveEntryListener(i);
- }
-}
+NetworkTable::~NetworkTable() = default;
NetworkTableInstance NetworkTable::GetInstance() const {
return NetworkTableInstance{m_inst};
@@ -99,78 +107,58 @@
return NetworkTableEntry{entry};
}
-NT_EntryListener NetworkTable::AddEntryListener(TableEntryListener listener,
- unsigned int flags) const {
- size_t prefix_len = m_path.size() + 1;
- return nt::AddEntryListener(
- m_inst, fmt::format("{}/", m_path),
- [=](const EntryNotification& event) {
- auto relative_key = wpi::substr(event.name, prefix_len);
- if (relative_key.find(PATH_SEPARATOR_CHAR) != std::string_view::npos) {
- return;
- }
- listener(const_cast<NetworkTable*>(this), relative_key,
- NetworkTableEntry{event.entry}, event.value, event.flags);
- },
- flags);
+Topic NetworkTable::GetTopic(std::string_view name) const {
+ fmt::memory_buffer buf;
+ fmt::format_to(fmt::appender{buf}, "{}/{}", m_path, name);
+ return Topic{::nt::GetTopic(m_inst, {buf.data(), buf.size()})};
}
-NT_EntryListener NetworkTable::AddEntryListener(std::string_view key,
- TableEntryListener listener,
- unsigned int flags) const {
- size_t prefix_len = m_path.size() + 1;
- auto entry = GetEntry(key);
- return nt::AddEntryListener(
- entry.GetHandle(),
- [=](const EntryNotification& event) {
- listener(const_cast<NetworkTable*>(this),
- wpi::substr(event.name, prefix_len), entry, event.value,
- event.flags);
- },
- flags);
+BooleanTopic NetworkTable::GetBooleanTopic(std::string_view name) const {
+ return BooleanTopic{GetTopic(name)};
}
-void NetworkTable::RemoveEntryListener(NT_EntryListener listener) const {
- nt::RemoveEntryListener(listener);
+IntegerTopic NetworkTable::GetIntegerTopic(std::string_view name) const {
+ return IntegerTopic{GetTopic(name)};
}
-NT_EntryListener NetworkTable::AddSubTableListener(TableListener listener,
- bool localNotify) {
- size_t prefix_len = m_path.size() + 1;
-
- // The lambda needs to be copyable, but StringMap is not, so use
- // a shared_ptr to it.
- auto notified_tables = std::make_shared<wpi::StringMap<char>>();
-
- unsigned int flags = NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE;
- if (localNotify) {
- flags |= NT_NOTIFY_LOCAL;
- }
- NT_EntryListener id = nt::AddEntryListener(
- m_inst, fmt::format("{}/", m_path),
- [=](const EntryNotification& event) {
- auto relative_key = wpi::substr(event.name, prefix_len);
- auto end_sub_table = relative_key.find(PATH_SEPARATOR_CHAR);
- if (end_sub_table == std::string_view::npos) {
- return;
- }
- auto sub_table_key = wpi::substr(relative_key, 0, end_sub_table);
- if (notified_tables->find(sub_table_key) == notified_tables->end()) {
- return;
- }
- notified_tables->insert(std::make_pair(sub_table_key, '\0'));
- listener(this, sub_table_key, this->GetSubTable(sub_table_key));
- },
- flags);
- m_listeners.emplace_back(id);
- return id;
+FloatTopic NetworkTable::GetFloatTopic(std::string_view name) const {
+ return FloatTopic{GetTopic(name)};
}
-void NetworkTable::RemoveTableListener(NT_EntryListener listener) {
- nt::RemoveEntryListener(listener);
- auto matches_begin =
- std::remove(m_listeners.begin(), m_listeners.end(), listener);
- m_listeners.erase(matches_begin, m_listeners.end());
+DoubleTopic NetworkTable::GetDoubleTopic(std::string_view name) const {
+ return DoubleTopic{GetTopic(name)};
+}
+
+StringTopic NetworkTable::GetStringTopic(std::string_view name) const {
+ return StringTopic{GetTopic(name)};
+}
+
+RawTopic NetworkTable::GetRawTopic(std::string_view name) const {
+ return RawTopic{GetTopic(name)};
+}
+
+BooleanArrayTopic NetworkTable::GetBooleanArrayTopic(
+ std::string_view name) const {
+ return BooleanArrayTopic{GetTopic(name)};
+}
+
+IntegerArrayTopic NetworkTable::GetIntegerArrayTopic(
+ std::string_view name) const {
+ return IntegerArrayTopic{GetTopic(name)};
+}
+
+FloatArrayTopic NetworkTable::GetFloatArrayTopic(std::string_view name) const {
+ return FloatArrayTopic{GetTopic(name)};
+}
+
+DoubleArrayTopic NetworkTable::GetDoubleArrayTopic(
+ std::string_view name) const {
+ return DoubleArrayTopic{GetTopic(name)};
+}
+
+StringArrayTopic NetworkTable::GetStringArrayTopic(
+ std::string_view name) const {
+ return StringArrayTopic{GetTopic(name)};
}
std::shared_ptr<NetworkTable> NetworkTable::GetSubTable(
@@ -183,25 +171,52 @@
if (key.empty()) {
return false;
}
- return GetEntry(key).Exists();
+ return GetTopic(key).Exists();
}
bool NetworkTable::ContainsSubTable(std::string_view key) const {
- return !GetEntryInfo(m_inst, fmt::format("{}/{}/", m_path, key), 0).empty();
+ return !::nt::GetTopics(m_inst, fmt::format("{}/{}/", m_path, key), 0)
+ .empty();
+}
+
+std::vector<TopicInfo> NetworkTable::GetTopicInfo(int types) const {
+ std::vector<TopicInfo> infos;
+ size_t prefix_len = m_path.size() + 1;
+ for (auto&& info :
+ ::nt::GetTopicInfo(m_inst, fmt::format("{}/", m_path), types)) {
+ auto relative_key = wpi::substr(info.name, prefix_len);
+ if (relative_key.find(PATH_SEPARATOR_CHAR) != std::string_view::npos) {
+ continue;
+ }
+ infos.emplace_back(std::move(info));
+ }
+ return infos;
+}
+
+std::vector<Topic> NetworkTable::GetTopics(int types) const {
+ std::vector<Topic> topics;
+ size_t prefix_len = m_path.size() + 1;
+ for (auto&& info :
+ ::nt::GetTopicInfo(m_inst, fmt::format("{}/", m_path), types)) {
+ auto relative_key = wpi::substr(info.name, prefix_len);
+ if (relative_key.find(PATH_SEPARATOR_CHAR) != std::string_view::npos) {
+ continue;
+ }
+ topics.emplace_back(info.topic);
+ }
+ return topics;
}
std::vector<std::string> NetworkTable::GetKeys(int types) const {
std::vector<std::string> keys;
size_t prefix_len = m_path.size() + 1;
- auto infos = GetEntryInfo(m_inst, fmt::format("{}/", m_path), types);
- std::scoped_lock lock(m_mutex);
- for (auto& info : infos) {
+ for (auto&& info :
+ ::nt::GetTopicInfo(m_inst, fmt::format("{}/", m_path), types)) {
auto relative_key = wpi::substr(info.name, prefix_len);
if (relative_key.find(PATH_SEPARATOR_CHAR) != std::string_view::npos) {
continue;
}
keys.emplace_back(relative_key);
- m_entries[relative_key] = info.entry;
}
return keys;
}
@@ -209,8 +224,9 @@
std::vector<std::string> NetworkTable::GetSubTables() const {
std::vector<std::string> keys;
size_t prefix_len = m_path.size() + 1;
- for (auto& entry : GetEntryInfo(m_inst, fmt::format("{}/", m_path), 0)) {
- auto relative_key = wpi::substr(entry.name, prefix_len);
+ for (auto&& topic :
+ ::nt::GetTopicInfo(m_inst, fmt::format("{}/", m_path), 0)) {
+ auto relative_key = wpi::substr(topic.name, prefix_len);
size_t end_subtable = relative_key.find(PATH_SEPARATOR_CHAR);
if (end_subtable == std::string_view::npos) {
continue;
@@ -232,22 +248,6 @@
return GetEntry(key).IsPersistent();
}
-void NetworkTable::SetFlags(std::string_view key, unsigned int flags) {
- GetEntry(key).SetFlags(flags);
-}
-
-void NetworkTable::ClearFlags(std::string_view key, unsigned int flags) {
- GetEntry(key).ClearFlags(flags);
-}
-
-unsigned int NetworkTable::GetFlags(std::string_view key) const {
- return GetEntry(key).GetFlags();
-}
-
-void NetworkTable::Delete(std::string_view key) {
- GetEntry(key).Delete();
-}
-
bool NetworkTable::PutNumber(std::string_view key, double value) {
return GetEntry(key).SetDouble(value);
}
@@ -288,75 +288,75 @@
}
bool NetworkTable::PutBooleanArray(std::string_view key,
- wpi::span<const int> value) {
+ std::span<const int> value) {
return GetEntry(key).SetBooleanArray(value);
}
bool NetworkTable::SetDefaultBooleanArray(std::string_view key,
- wpi::span<const int> defaultValue) {
+ std::span<const int> defaultValue) {
return GetEntry(key).SetDefaultBooleanArray(defaultValue);
}
std::vector<int> NetworkTable::GetBooleanArray(
- std::string_view key, wpi::span<const int> defaultValue) const {
+ std::string_view key, std::span<const int> defaultValue) const {
return GetEntry(key).GetBooleanArray(defaultValue);
}
bool NetworkTable::PutNumberArray(std::string_view key,
- wpi::span<const double> value) {
+ std::span<const double> value) {
return GetEntry(key).SetDoubleArray(value);
}
bool NetworkTable::SetDefaultNumberArray(std::string_view key,
- wpi::span<const double> defaultValue) {
+ std::span<const double> defaultValue) {
return GetEntry(key).SetDefaultDoubleArray(defaultValue);
}
std::vector<double> NetworkTable::GetNumberArray(
- std::string_view key, wpi::span<const double> defaultValue) const {
+ std::string_view key, std::span<const double> defaultValue) const {
return GetEntry(key).GetDoubleArray(defaultValue);
}
bool NetworkTable::PutStringArray(std::string_view key,
- wpi::span<const std::string> value) {
+ std::span<const std::string> value) {
return GetEntry(key).SetStringArray(value);
}
bool NetworkTable::SetDefaultStringArray(
- std::string_view key, wpi::span<const std::string> defaultValue) {
+ std::string_view key, std::span<const std::string> defaultValue) {
return GetEntry(key).SetDefaultStringArray(defaultValue);
}
std::vector<std::string> NetworkTable::GetStringArray(
- std::string_view key, wpi::span<const std::string> defaultValue) const {
+ std::string_view key, std::span<const std::string> defaultValue) const {
return GetEntry(key).GetStringArray(defaultValue);
}
-bool NetworkTable::PutRaw(std::string_view key, std::string_view value) {
+bool NetworkTable::PutRaw(std::string_view key,
+ std::span<const uint8_t> value) {
return GetEntry(key).SetRaw(value);
}
bool NetworkTable::SetDefaultRaw(std::string_view key,
- std::string_view defaultValue) {
+ std::span<const uint8_t> defaultValue) {
return GetEntry(key).SetDefaultRaw(defaultValue);
}
-std::string NetworkTable::GetRaw(std::string_view key,
- std::string_view defaultValue) const {
+std::vector<uint8_t> NetworkTable::GetRaw(
+ std::string_view key, std::span<const uint8_t> defaultValue) const {
return GetEntry(key).GetRaw(defaultValue);
}
-bool NetworkTable::PutValue(std::string_view key,
- std::shared_ptr<Value> value) {
+bool NetworkTable::PutValue(std::string_view key, const Value& value) {
return GetEntry(key).SetValue(value);
}
bool NetworkTable::SetDefaultValue(std::string_view key,
- std::shared_ptr<Value> defaultValue) {
+ const Value& defaultValue) {
return GetEntry(key).SetDefaultValue(defaultValue);
}
-std::shared_ptr<Value> NetworkTable::GetValue(std::string_view key) const {
+Value NetworkTable::GetValue(std::string_view key) const {
return GetEntry(key).GetValue();
}
@@ -364,12 +364,63 @@
return m_path;
}
-const char* NetworkTable::SaveEntries(std::string_view filename) const {
- return nt::SaveEntries(m_inst, filename, fmt::format("{}/", m_path));
+NT_Listener NetworkTable::AddListener(int eventMask,
+ TableEventListener listener) {
+ return NetworkTableInstance{m_inst}.AddListener(
+ {{fmt::format("{}/", m_path)}}, eventMask,
+ [this, cb = std::move(listener)](const Event& event) {
+ std::string topicNameStr;
+ std::string_view topicName;
+ if (auto topicInfo = event.GetTopicInfo()) {
+ topicName = topicInfo->name;
+ } else if (auto valueData = event.GetValueEventData()) {
+ topicNameStr = Topic{valueData->topic}.GetName();
+ topicName = topicNameStr;
+ } else {
+ return;
+ }
+ auto relative_key = wpi::substr(topicName, m_path.size() + 1);
+ if (relative_key.find(PATH_SEPARATOR_CHAR) != std::string_view::npos) {
+ return;
+ }
+ cb(this, relative_key, event);
+ });
}
-const char* NetworkTable::LoadEntries(
- std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn) {
- return nt::LoadEntries(m_inst, filename, fmt::format("{}/", m_path), warn);
+NT_Listener NetworkTable::AddListener(std::string_view key, int eventMask,
+ TableEventListener listener) {
+ return NetworkTableInstance{m_inst}.AddListener(
+ GetEntry(key), eventMask,
+ [this, cb = std::move(listener),
+ key = std::string{key}](const Event& event) { cb(this, key, event); });
+}
+
+NT_Listener NetworkTable::AddSubTableListener(SubTableListener listener) {
+ // The lambda needs to be copyable, but StringMap is not, so use
+ // a shared_ptr to it.
+ auto notified_tables = std::make_shared<wpi::StringMap<char>>();
+
+ return NetworkTableInstance{m_inst}.AddListener(
+ {{fmt::format("{}/", m_path)}}, NT_EVENT_PUBLISH | NT_EVENT_IMMEDIATE,
+ [this, cb = std::move(listener), notified_tables](const Event& event) {
+ auto topicInfo = event.GetTopicInfo();
+ if (!topicInfo) {
+ return;
+ }
+ auto relative_key = wpi::substr(topicInfo->name, m_path.size() + 1);
+ auto end_sub_table = relative_key.find(PATH_SEPARATOR_CHAR);
+ if (end_sub_table == std::string_view::npos) {
+ return;
+ }
+ auto sub_table_key = relative_key.substr(0, end_sub_table);
+ if (notified_tables->find(sub_table_key) != notified_tables->end()) {
+ return;
+ }
+ notified_tables->insert(std::make_pair(sub_table_key, '\0'));
+ cb(this, sub_table_key, this->GetSubTable(sub_table_key));
+ });
+}
+
+void NetworkTable::RemoveListener(NT_Listener listener) {
+ NetworkTableInstance{m_inst}.RemoveListener(listener);
}
diff --git a/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp b/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp
index abe64e5..12bb5d8 100644
--- a/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp
+++ b/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp
@@ -5,9 +5,14 @@
#include "networktables/NetworkTableEntry.h"
#include "networktables/NetworkTableInstance.h"
+#include "networktables/Topic.h"
using namespace nt;
NetworkTableInstance NetworkTableEntry::GetInstance() const {
return NetworkTableInstance{GetInstanceFromHandle(m_handle)};
}
+
+Topic NetworkTableEntry::GetTopic() const {
+ return Topic{::nt::GetTopicFromHandle(m_handle)};
+}
diff --git a/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp b/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp
index 4566b30..251e211 100644
--- a/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp
+++ b/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp
@@ -7,8 +7,76 @@
#include <fmt/format.h>
#include <wpi/SmallVector.h>
+#include "networktables/BooleanArrayTopic.h"
+#include "networktables/BooleanTopic.h"
+#include "networktables/DoubleArrayTopic.h"
+#include "networktables/DoubleTopic.h"
+#include "networktables/FloatArrayTopic.h"
+#include "networktables/FloatTopic.h"
+#include "networktables/IntegerArrayTopic.h"
+#include "networktables/IntegerTopic.h"
+#include "networktables/MultiSubscriber.h"
+#include "networktables/RawTopic.h"
+#include "networktables/StringArrayTopic.h"
+#include "networktables/StringTopic.h"
+
using namespace nt;
+Topic NetworkTableInstance::GetTopic(std::string_view name) const {
+ return Topic{::nt::GetTopic(m_handle, name)};
+}
+
+BooleanTopic NetworkTableInstance::GetBooleanTopic(
+ std::string_view name) const {
+ return BooleanTopic{GetTopic(name)};
+}
+
+IntegerTopic NetworkTableInstance::GetIntegerTopic(
+ std::string_view name) const {
+ return IntegerTopic{GetTopic(name)};
+}
+
+FloatTopic NetworkTableInstance::GetFloatTopic(std::string_view name) const {
+ return FloatTopic{GetTopic(name)};
+}
+
+DoubleTopic NetworkTableInstance::GetDoubleTopic(std::string_view name) const {
+ return DoubleTopic{GetTopic(name)};
+}
+
+StringTopic NetworkTableInstance::GetStringTopic(std::string_view name) const {
+ return StringTopic{GetTopic(name)};
+}
+
+RawTopic NetworkTableInstance::GetRawTopic(std::string_view name) const {
+ return RawTopic{GetTopic(name)};
+}
+
+BooleanArrayTopic NetworkTableInstance::GetBooleanArrayTopic(
+ std::string_view name) const {
+ return BooleanArrayTopic{GetTopic(name)};
+}
+
+IntegerArrayTopic NetworkTableInstance::GetIntegerArrayTopic(
+ std::string_view name) const {
+ return IntegerArrayTopic{GetTopic(name)};
+}
+
+FloatArrayTopic NetworkTableInstance::GetFloatArrayTopic(
+ std::string_view name) const {
+ return FloatArrayTopic{GetTopic(name)};
+}
+
+DoubleArrayTopic NetworkTableInstance::GetDoubleArrayTopic(
+ std::string_view name) const {
+ return DoubleArrayTopic{GetTopic(name)};
+}
+
+StringArrayTopic NetworkTableInstance::GetStringArrayTopic(
+ std::string_view name) const {
+ return StringArrayTopic{GetTopic(name)};
+}
+
std::shared_ptr<NetworkTable> NetworkTableInstance::GetTable(
std::string_view key) const {
if (key.empty() || key == "/") {
@@ -23,33 +91,54 @@
}
}
-void NetworkTableInstance::StartClient(
- wpi::span<const std::string_view> servers, unsigned int port) {
- wpi::SmallVector<std::pair<std::string_view, unsigned int>, 8> server_ports;
- for (const auto& server : servers) {
- server_ports.emplace_back(std::make_pair(server, port));
- }
- StartClient(server_ports);
-}
-
-void NetworkTableInstance::SetServer(wpi::span<const std::string_view> servers,
+void NetworkTableInstance::SetServer(std::span<const std::string_view> servers,
unsigned int port) {
- wpi::SmallVector<std::pair<std::string_view, unsigned int>, 8> server_ports;
+ std::vector<std::pair<std::string_view, unsigned int>> serversArr;
+ serversArr.reserve(servers.size());
for (const auto& server : servers) {
- server_ports.emplace_back(std::make_pair(server, port));
+ serversArr.emplace_back(std::string{server}, port);
}
- SetServer(server_ports);
+ SetServer(serversArr);
}
-NT_EntryListener NetworkTableInstance::AddEntryListener(
- std::string_view prefix,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const {
- return ::nt::AddEntryListener(m_handle, prefix, callback, flags);
+NT_Listener NetworkTableInstance::AddListener(Topic topic,
+ unsigned int eventMask,
+ ListenerCallback listener) {
+ if (::nt::GetInstanceFromHandle(topic.GetHandle()) != m_handle) {
+ fmt::print(stderr, "AddListener: topic is not from this instance\n");
+ return 0;
+ }
+ return ::nt::AddListener(topic.GetHandle(), eventMask, std::move(listener));
}
-NT_ConnectionListener NetworkTableInstance::AddConnectionListener(
- std::function<void(const ConnectionNotification& event)> callback,
- bool immediate_notify) const {
- return ::nt::AddConnectionListener(m_handle, callback, immediate_notify);
+NT_Listener NetworkTableInstance::AddListener(Subscriber& subscriber,
+ unsigned int eventMask,
+ ListenerCallback listener) {
+ if (::nt::GetInstanceFromHandle(subscriber.GetHandle()) != m_handle) {
+ fmt::print(stderr, "AddListener: subscriber is not from this instance\n");
+ return 0;
+ }
+ return ::nt::AddListener(subscriber.GetHandle(), eventMask,
+ std::move(listener));
+}
+
+NT_Listener NetworkTableInstance::AddListener(const NetworkTableEntry& entry,
+ int eventMask,
+ ListenerCallback listener) {
+ if (::nt::GetInstanceFromHandle(entry.GetHandle()) != m_handle) {
+ fmt::print(stderr, "AddListener: entry is not from this instance\n");
+ return 0;
+ }
+ return ::nt::AddListener(entry.GetHandle(), eventMask, std::move(listener));
+}
+
+NT_Listener NetworkTableInstance::AddListener(MultiSubscriber& subscriber,
+ int eventMask,
+ ListenerCallback listener) {
+ if (::nt::GetInstanceFromHandle(subscriber.GetHandle()) != m_handle) {
+ fmt::print(stderr, "AddListener: subscriber is not from this instance\n");
+ return 0;
+ }
+ return ::nt::AddListener(subscriber.GetHandle(), eventMask,
+ std::move(listener));
}
diff --git a/ntcore/src/main/native/cpp/networktables/RpcCall.cpp b/ntcore/src/main/native/cpp/networktables/RpcCall.cpp
deleted file mode 100644
index 2192a82..0000000
--- a/ntcore/src/main/native/cpp/networktables/RpcCall.cpp
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "networktables/RpcCall.h"
-
-#include "networktables/NetworkTableEntry.h"
-
-using namespace nt;
-
-NetworkTableEntry RpcCall::GetEntry() const {
- return NetworkTableEntry{m_entry};
-}
diff --git a/ntcore/src/main/native/cpp/networktables/Topic.cpp b/ntcore/src/main/native/cpp/networktables/Topic.cpp
new file mode 100644
index 0000000..e3f6ac0
--- /dev/null
+++ b/ntcore/src/main/native/cpp/networktables/Topic.cpp
@@ -0,0 +1,57 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "networktables/Topic.h"
+
+#include <wpi/json.h>
+
+#include "networktables/GenericEntry.h"
+
+using namespace nt;
+
+wpi::json Topic::GetProperty(std::string_view name) const {
+ return ::nt::GetTopicProperty(m_handle, name);
+}
+
+void Topic::SetProperty(std::string_view name, const wpi::json& value) {
+ ::nt::SetTopicProperty(m_handle, name, value);
+}
+
+wpi::json Topic::GetProperties() const {
+ return ::nt::GetTopicProperties(m_handle);
+}
+
+GenericSubscriber Topic::GenericSubscribe(const PubSubOptions& options) {
+ return GenericSubscribe("", options);
+}
+
+GenericSubscriber Topic::GenericSubscribe(std::string_view typeString,
+ const PubSubOptions& options) {
+ return GenericSubscriber{::nt::Subscribe(
+ m_handle, ::nt::GetTypeFromString(typeString), typeString, options)};
+}
+
+GenericPublisher Topic::GenericPublish(std::string_view typeString,
+ const PubSubOptions& options) {
+ return GenericPublisher{::nt::Publish(
+ m_handle, ::nt::GetTypeFromString(typeString), typeString, options)};
+}
+
+GenericPublisher Topic::GenericPublishEx(std::string_view typeString,
+ const wpi::json& properties,
+ const PubSubOptions& options) {
+ return GenericPublisher{::nt::PublishEx(m_handle,
+ ::nt::GetTypeFromString(typeString),
+ typeString, properties, options)};
+}
+
+GenericEntry Topic::GetGenericEntry(const PubSubOptions& options) {
+ return GetGenericEntry("", options);
+}
+
+GenericEntry Topic::GetGenericEntry(std::string_view typeString,
+ const PubSubOptions& options) {
+ return GenericEntry{::nt::GetEntry(
+ m_handle, ::nt::GetTypeFromString(typeString), typeString, options)};
+}
diff --git a/ntcore/src/main/native/cpp/ntcore_c.cpp b/ntcore/src/main/native/cpp/ntcore_c.cpp
index d910892..151e0f1 100644
--- a/ntcore/src/main/native/cpp/ntcore_c.cpp
+++ b/ntcore/src/main/native/cpp/ntcore_c.cpp
@@ -2,6 +2,8 @@
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
+#include "ntcore_c.h"
+
#include <stdint.h>
#include <cassert>
@@ -10,27 +12,24 @@
#include <string_view>
#include <wpi/MemAlloc.h>
+#include <wpi/SmallVector.h>
+#include <wpi/json.h>
#include <wpi/timestamp.h>
#include "Value_internal.h"
#include "ntcore.h"
+#include "ntcore_cpp.h"
using namespace nt;
// Conversion helpers
-static void ConvertToC(std::string_view in, char** out) {
- *out = static_cast<char*>(wpi::safe_malloc(in.size() + 1));
- std::memmove(*out, in.data(), in.size()); // NOLINT
- (*out)[in.size()] = '\0';
-}
-
-static void ConvertToC(const EntryInfo& in, NT_EntryInfo* out) {
- out->entry = in.entry;
+static void ConvertToC(const TopicInfo& in, NT_TopicInfo* out) {
+ out->topic = in.topic;
ConvertToC(in.name, &out->name);
out->type = in.type;
- out->flags = in.flags;
- out->last_change = in.last_change;
+ ConvertToC(in.type_str, &out->type_str);
+ ConvertToC(in.properties, &out->properties);
}
static void ConvertToC(const ConnectionInfo& in, NT_ConnectionInfo* out) {
@@ -41,80 +40,50 @@
out->protocol_version = in.protocol_version;
}
-static void ConvertToC(const RpcParamDef& in, NT_RpcParamDef* out) {
- ConvertToC(in.name, &out->name);
- ConvertToC(*in.def_value, &out->def_value);
-}
-
-static void ConvertToC(const RpcResultDef& in, NT_RpcResultDef* out) {
- ConvertToC(in.name, &out->name);
- out->type = in.type;
-}
-
-static void ConvertToC(const RpcDefinition& in, NT_RpcDefinition* out) {
- out->version = in.version;
- ConvertToC(in.name, &out->name);
-
- out->num_params = in.params.size();
- out->params = static_cast<NT_RpcParamDef*>(
- wpi::safe_malloc(in.params.size() * sizeof(NT_RpcParamDef)));
- for (size_t i = 0; i < in.params.size(); ++i) {
- ConvertToC(in.params[i], &out->params[i]);
- }
-
- out->num_results = in.results.size();
- out->results = static_cast<NT_RpcResultDef*>(
- wpi::safe_malloc(in.results.size() * sizeof(NT_RpcResultDef)));
- for (size_t i = 0; i < in.results.size(); ++i) {
- ConvertToC(in.results[i], &out->results[i]);
- }
-}
-
-static void ConvertToC(const RpcAnswer& in, NT_RpcAnswer* out) {
- out->entry = in.entry;
- out->call = in.call;
- ConvertToC(in.name, &out->name);
- ConvertToC(in.params, &out->params);
- ConvertToC(in.conn, &out->conn);
-}
-
-static void ConvertToC(const EntryNotification& in, NT_EntryNotification* out) {
- out->listener = in.listener;
- out->entry = in.entry;
- ConvertToC(in.name, &out->name);
- ConvertToC(*in.value, &out->value);
- out->flags = in.flags;
-}
-
-static void ConvertToC(const ConnectionNotification& in,
- NT_ConnectionNotification* out) {
- out->listener = in.listener;
- out->connected = in.connected;
- ConvertToC(in.conn, &out->conn);
+static void ConvertToC(const ValueEventData& in, NT_ValueEventData* out) {
+ out->topic = in.topic;
+ out->subentry = in.subentry;
+ ConvertToC(in.value, &out->value);
}
static void ConvertToC(const LogMessage& in, NT_LogMessage* out) {
- out->logger = in.logger;
out->level = in.level;
ConvertToC(in.filename, &out->filename);
out->line = in.line;
ConvertToC(in.message, &out->message);
}
-template <typename O, typename I>
-static O* ConvertToC(const std::vector<I>& in, size_t* out_len) {
- if (!out_len) {
- return nullptr;
+static void ConvertToC(const TimeSyncEventData& in, NT_TimeSyncEventData* out) {
+ out->serverTimeOffset = in.serverTimeOffset;
+ out->rtt2 = in.rtt2;
+ out->valid = in.valid;
+}
+
+static void ConvertToC(const Event& in, NT_Event* out) {
+ out->listener = in.listener;
+ out->flags = in.flags;
+ if ((in.flags & NT_EVENT_VALUE_ALL) != 0) {
+ if (auto v = in.GetValueEventData()) {
+ return ConvertToC(*v, &out->data.valueData);
+ }
+ } else if ((in.flags & NT_EVENT_TOPIC) != 0) {
+ if (auto v = in.GetTopicInfo()) {
+ return ConvertToC(*v, &out->data.topicInfo);
+ }
+ } else if ((in.flags & NT_EVENT_CONNECTION) != 0) {
+ if (auto v = in.GetConnectionInfo()) {
+ return ConvertToC(*v, &out->data.connInfo);
+ }
+ } else if ((in.flags & NT_EVENT_LOGMESSAGE) != 0) {
+ if (auto v = in.GetLogMessage()) {
+ return ConvertToC(*v, &out->data.logMessage);
+ }
+ } else if ((in.flags & NT_EVENT_TIMESYNC) != 0) {
+ if (auto v = in.GetTimeSyncEventData()) {
+ return ConvertToC(*v, &out->data.timeSyncData);
+ }
}
- *out_len = in.size();
- if (in.empty()) {
- return nullptr;
- }
- O* out = static_cast<O*>(wpi::safe_malloc(sizeof(O) * in.size()));
- for (size_t i = 0; i < in.size(); ++i) {
- ConvertToC(in[i], &out[i]);
- }
- return out;
+ out->flags = NT_EVENT_NONE; // sanity to make sure we don't dispose
}
static void DisposeConnectionInfo(NT_ConnectionInfo* info) {
@@ -122,48 +91,41 @@
std::free(info->remote_ip.str);
}
-static void DisposeEntryInfo(NT_EntryInfo* info) {
+static void DisposeTopicInfo(NT_TopicInfo* info) {
std::free(info->name.str);
+ std::free(info->type_str.str);
+ std::free(info->properties.str);
}
-static void DisposeEntryNotification(NT_EntryNotification* info) {
- std::free(info->name.str);
- NT_DisposeValue(&info->value);
+static void DisposeLogMessage(NT_LogMessage* msg) {
+ std::free(msg->filename);
+ std::free(msg->message);
}
-static void DisposeConnectionNotification(NT_ConnectionNotification* info) {
- DisposeConnectionInfo(&info->conn);
-}
-
-static RpcParamDef ConvertFromC(const NT_RpcParamDef& in) {
- RpcParamDef out;
- out.name = ConvertFromC(in.name);
- out.def_value = ConvertFromC(in.def_value);
- return out;
-}
-
-static RpcResultDef ConvertFromC(const NT_RpcResultDef& in) {
- RpcResultDef out;
- out.name = ConvertFromC(in.name);
- out.type = in.type;
- return out;
-}
-
-static RpcDefinition ConvertFromC(const NT_RpcDefinition& in) {
- RpcDefinition out;
- out.version = in.version;
- out.name = ConvertFromC(in.name);
-
- out.params.reserve(in.num_params);
- for (size_t i = 0; i < in.num_params; ++i) {
- out.params.push_back(ConvertFromC(in.params[i]));
+static void DisposeEvent(NT_Event* event) {
+ if ((event->flags & NT_EVENT_VALUE_ALL) != 0) {
+ NT_DisposeValue(&event->data.valueData.value);
+ } else if ((event->flags & NT_EVENT_TOPIC) != 0) {
+ DisposeTopicInfo(&event->data.topicInfo);
+ } else if ((event->flags & NT_EVENT_CONNECTION) != 0) {
+ DisposeConnectionInfo(&event->data.connInfo);
+ } else if ((event->flags & NT_EVENT_LOGMESSAGE) != 0) {
+ DisposeLogMessage(&event->data.logMessage);
}
+}
- out.results.reserve(in.num_results);
- for (size_t i = 0; i < in.num_results; ++i) {
- out.results.push_back(ConvertFromC(in.results[i]));
- }
-
+static PubSubOptions ConvertToCpp(const NT_PubSubOptions* in) {
+ PubSubOptions out;
+ out.pollStorage = in->pollStorage;
+ out.periodic = in->periodic;
+ out.excludePublisher = in->excludePublisher;
+ out.sendAll = in->sendAll;
+ out.topicsOnly = in->topicsOnly;
+ out.prefixMatch = in->prefixMatch;
+ out.keepDuplicates = in->keepDuplicates;
+ out.disableRemote = in->disableRemote;
+ out.disableLocal = in->disableLocal;
+ out.excludeSelf = in->excludeSelf;
return out;
}
@@ -197,21 +159,6 @@
return nt::GetEntry(inst, {name, name_len});
}
-NT_Entry* NT_GetEntries(NT_Inst inst, const char* prefix, size_t prefix_len,
- unsigned int types, size_t* count) {
- auto info_v = nt::GetEntries(inst, {prefix, prefix_len}, types);
- *count = info_v.size();
- if (info_v.size() == 0) {
- return nullptr;
- }
-
- // create array and copy into it
- NT_Entry* info = static_cast<NT_Entry*>(
- wpi::safe_malloc(info_v.size() * sizeof(NT_Entry)));
- std::memcpy(info, info_v.data(), info_v.size() * sizeof(NT_Entry));
- return info;
-}
-
char* NT_GetEntryName(NT_Entry entry, size_t* name_len) {
struct NT_String v_name;
nt::ConvertToC(nt::GetEntryName(entry), &v_name);
@@ -233,7 +180,7 @@
if (!v) {
return;
}
- ConvertToC(*v, value);
+ ConvertToC(v, value);
}
int NT_SetDefaultEntryValue(NT_Entry entry,
@@ -245,10 +192,6 @@
return nt::SetEntryValue(entry, ConvertFromC(*value));
}
-void NT_SetEntryTypeValue(NT_Entry entry, const struct NT_Value* value) {
- nt::SetEntryTypeValue(entry, ConvertFromC(*value));
-}
-
void NT_SetEntryFlags(NT_Entry entry, unsigned int flags) {
nt::SetEntryFlags(entry, flags);
}
@@ -257,23 +200,50 @@
return nt::GetEntryFlags(entry);
}
-void NT_DeleteEntry(NT_Entry entry) {
- nt::DeleteEntry(entry);
+struct NT_Value* NT_ReadQueueValue(NT_Handle subentry, size_t* count) {
+ return ConvertToC<NT_Value>(nt::ReadQueueValue(subentry), count);
}
-void NT_DeleteAllEntries(NT_Inst inst) {
- nt::DeleteAllEntries(inst);
+NT_Topic* NT_GetTopics(NT_Inst inst, const char* prefix, size_t prefix_len,
+ unsigned int types, size_t* count) {
+ auto info_v = nt::GetTopics(inst, {prefix, prefix_len}, types);
+ return ConvertToC<NT_Topic>(info_v, count);
}
-struct NT_EntryInfo* NT_GetEntryInfo(NT_Inst inst, const char* prefix,
- size_t prefix_len, unsigned int types,
- size_t* count) {
- auto info_v = nt::GetEntryInfo(inst, {prefix, prefix_len}, types);
- return ConvertToC<NT_EntryInfo>(info_v, count);
+NT_Topic* NT_GetTopicsStr(NT_Inst inst, const char* prefix, size_t prefix_len,
+ const char* const* types, size_t types_len,
+ size_t* count) {
+ wpi::SmallVector<std::string_view, 4> typesCpp;
+ typesCpp.reserve(types_len);
+ for (size_t i = 0; i < types_len; ++i) {
+ typesCpp.emplace_back(types[i]);
+ }
+ auto info_v = nt::GetTopics(inst, {prefix, prefix_len}, typesCpp);
+ return ConvertToC<NT_Topic>(info_v, count);
}
-NT_Bool NT_GetEntryInfoHandle(NT_Entry entry, struct NT_EntryInfo* info) {
- auto info_v = nt::GetEntryInfo(entry);
+struct NT_TopicInfo* NT_GetTopicInfos(NT_Inst inst, const char* prefix,
+ size_t prefix_len, unsigned int types,
+ size_t* count) {
+ auto info_v = nt::GetTopicInfo(inst, {prefix, prefix_len}, types);
+ return ConvertToC<NT_TopicInfo>(info_v, count);
+}
+
+struct NT_TopicInfo* NT_GetTopicInfosStr(NT_Inst inst, const char* prefix,
+ size_t prefix_len,
+ const char* const* types,
+ size_t types_len, size_t* count) {
+ wpi::SmallVector<std::string_view, 4> typesCpp;
+ typesCpp.reserve(types_len);
+ for (size_t i = 0; i < types_len; ++i) {
+ typesCpp.emplace_back(types[i]);
+ }
+ auto info_v = nt::GetTopicInfo(inst, {prefix, prefix_len}, typesCpp);
+ return ConvertToC<NT_TopicInfo>(info_v, count);
+}
+
+NT_Bool NT_GetTopicInfo(NT_Topic topic, struct NT_TopicInfo* info) {
+ auto info_v = nt::GetTopicInfo(topic);
if (info_v.name.empty()) {
return false;
}
@@ -281,299 +251,240 @@
return true;
}
+NT_Topic NT_GetTopic(NT_Inst inst, const char* name, size_t name_len) {
+ return nt::GetTopic(inst, std::string_view{name, name_len});
+}
+
+char* NT_GetTopicName(NT_Topic topic, size_t* name_len) {
+ auto name = nt::GetTopicName(topic);
+ if (name.empty()) {
+ *name_len = 0;
+ return nullptr;
+ }
+ struct NT_String v_name;
+ nt::ConvertToC(name, &v_name);
+ *name_len = v_name.len;
+ return v_name.str;
+}
+
+NT_Type NT_GetTopicType(NT_Topic topic) {
+ return nt::GetTopicType(topic);
+}
+
+char* NT_GetTopicTypeString(NT_Topic topic, size_t* type_len) {
+ auto type = nt::GetTopicTypeString(topic);
+ struct NT_String v_type;
+ nt::ConvertToC(type, &v_type);
+ *type_len = v_type.len;
+ return v_type.str;
+}
+
+void NT_SetTopicPersistent(NT_Topic topic, NT_Bool value) {
+ nt::SetTopicPersistent(topic, value);
+}
+
+NT_Bool NT_GetTopicPersistent(NT_Topic topic) {
+ return nt::GetTopicPersistent(topic);
+}
+
+void NT_SetTopicRetained(NT_Topic topic, NT_Bool value) {
+ nt::SetTopicRetained(topic, value);
+}
+
+NT_Bool NT_GetTopicRetained(NT_Topic topic) {
+ return nt::GetTopicRetained(topic);
+}
+
+NT_Bool NT_GetTopicExists(NT_Handle handle) {
+ return nt::GetTopicExists(handle);
+}
+
+char* NT_GetTopicProperty(NT_Topic topic, const char* name, size_t* len) {
+ wpi::json j = nt::GetTopicProperty(topic, name);
+ struct NT_String v;
+ nt::ConvertToC(j.dump(), &v);
+ *len = v.len;
+ return v.str;
+}
+
+NT_Bool NT_SetTopicProperty(NT_Topic topic, const char* name,
+ const char* value) {
+ wpi::json j;
+ try {
+ j = wpi::json::parse(value);
+ } catch (wpi::json::parse_error&) {
+ return false;
+ }
+ nt::SetTopicProperty(topic, name, j);
+ return true;
+}
+
+void NT_DeleteTopicProperty(NT_Topic topic, const char* name) {
+ nt::DeleteTopicProperty(topic, name);
+}
+
+char* NT_GetTopicProperties(NT_Topic topic, size_t* len) {
+ wpi::json j = nt::GetTopicProperties(topic);
+ struct NT_String v;
+ nt::ConvertToC(j.dump(), &v);
+ *len = v.len;
+ return v.str;
+}
+
+NT_Bool NT_SetTopicProperties(NT_Topic topic, const char* properties) {
+ wpi::json j;
+ try {
+ j = wpi::json::parse(properties);
+ } catch (wpi::json::parse_error&) {
+ return false;
+ }
+ return nt::SetTopicProperties(topic, j);
+}
+
+NT_Subscriber NT_Subscribe(NT_Topic topic, NT_Type type, const char* typeStr,
+ const struct NT_PubSubOptions* options) {
+ return nt::Subscribe(topic, type, typeStr, ConvertToCpp(options));
+}
+
+void NT_Unsubscribe(NT_Subscriber sub) {
+ return nt::Unsubscribe(sub);
+}
+
+NT_Publisher NT_Publish(NT_Topic topic, NT_Type type, const char* typeStr,
+ const struct NT_PubSubOptions* options) {
+ return nt::Publish(topic, type, typeStr, ConvertToCpp(options));
+}
+
+NT_Publisher NT_PublishEx(NT_Topic topic, NT_Type type, const char* typeStr,
+ const char* properties,
+ const struct NT_PubSubOptions* options) {
+ wpi::json j;
+ if (properties[0] == '\0') {
+ // gracefully handle empty string
+ j = wpi::json::object();
+ } else {
+ try {
+ j = wpi::json::parse(properties);
+ } catch (wpi::json::parse_error&) {
+ return {};
+ }
+ }
+
+ return nt::PublishEx(topic, type, typeStr, j, ConvertToCpp(options));
+}
+
+void NT_Unpublish(NT_Handle pubentry) {
+ return nt::Unpublish(pubentry);
+}
+
+NT_Entry NT_GetEntryEx(NT_Topic topic, NT_Type type, const char* typeStr,
+ const struct NT_PubSubOptions* options) {
+ return nt::GetEntry(topic, type, typeStr, ConvertToCpp(options));
+}
+
+void NT_ReleaseEntry(NT_Entry entry) {
+ nt::ReleaseEntry(entry);
+}
+
+void NT_Release(NT_Handle pubsubentry) {
+ nt::Release(pubsubentry);
+}
+
+NT_Topic NT_GetTopicFromHandle(NT_Handle pubsubentry) {
+ return nt::GetTopicFromHandle(pubsubentry);
+}
+
/*
* Callback Creation Functions
*/
-NT_EntryListener NT_AddEntryListener(NT_Inst inst, const char* prefix,
- size_t prefix_len, void* data,
- NT_EntryListenerCallback callback,
- unsigned int flags) {
- return nt::AddEntryListener(
- inst, {prefix, prefix_len},
- [=](const EntryNotification& event) {
- NT_EntryNotification c_event;
- ConvertToC(event, &c_event);
- callback(data, &c_event);
- DisposeEntryNotification(&c_event);
- },
- flags);
+NT_ListenerPoller NT_CreateListenerPoller(NT_Inst inst) {
+ return nt::CreateListenerPoller(inst);
}
-NT_EntryListener NT_AddEntryListenerSingle(NT_Entry entry, void* data,
- NT_EntryListenerCallback callback,
- unsigned int flags) {
- return nt::AddEntryListener(
- entry,
- [=](const EntryNotification& event) {
- NT_EntryNotification c_event;
- ConvertToC(event, &c_event);
- callback(data, &c_event);
- DisposeEntryNotification(&c_event);
- },
- flags);
+void NT_DestroyListenerPoller(NT_ListenerPoller poller) {
+ nt::DestroyListenerPoller(poller);
}
-NT_EntryListenerPoller NT_CreateEntryListenerPoller(NT_Inst inst) {
- return nt::CreateEntryListenerPoller(inst);
+struct NT_Event* NT_ReadListenerQueue(NT_ListenerPoller poller, size_t* len) {
+ auto arr_cpp = nt::ReadListenerQueue(poller);
+ return ConvertToC<NT_Event>(arr_cpp, len);
}
-void NT_DestroyEntryListenerPoller(NT_EntryListenerPoller poller) {
- nt::DestroyEntryListenerPoller(poller);
+void NT_RemoveListener(NT_Listener listener) {
+ nt::RemoveListener(listener);
}
-NT_EntryListener NT_AddPolledEntryListener(NT_EntryListenerPoller poller,
- const char* prefix,
- size_t prefix_len,
- unsigned int flags) {
- return nt::AddPolledEntryListener(poller, {prefix, prefix_len}, flags);
+NT_Bool NT_WaitForListenerQueue(NT_Handle handle, double timeout) {
+ return nt::WaitForListenerQueue(handle, timeout);
}
-NT_EntryListener NT_AddPolledEntryListenerSingle(NT_EntryListenerPoller poller,
- NT_Entry entry,
- unsigned int flags) {
- return nt::AddPolledEntryListener(poller, entry, flags);
-}
-
-struct NT_EntryNotification* NT_PollEntryListener(NT_EntryListenerPoller poller,
- size_t* len) {
- auto arr_cpp = nt::PollEntryListener(poller);
- return ConvertToC<NT_EntryNotification>(arr_cpp, len);
-}
-
-struct NT_EntryNotification* NT_PollEntryListenerTimeout(
- NT_EntryListenerPoller poller, size_t* len, double timeout,
- NT_Bool* timed_out) {
- bool cpp_timed_out = false;
- auto arr_cpp = nt::PollEntryListener(poller, timeout, &cpp_timed_out);
- *timed_out = cpp_timed_out;
- return ConvertToC<NT_EntryNotification>(arr_cpp, len);
-}
-
-void NT_CancelPollEntryListener(NT_EntryListenerPoller poller) {
- nt::CancelPollEntryListener(poller);
-}
-
-void NT_RemoveEntryListener(NT_EntryListener entry_listener) {
- nt::RemoveEntryListener(entry_listener);
-}
-
-NT_Bool NT_WaitForEntryListenerQueue(NT_Inst inst, double timeout) {
- return nt::WaitForEntryListenerQueue(inst, timeout);
-}
-
-NT_ConnectionListener NT_AddConnectionListener(
- NT_Inst inst, void* data, NT_ConnectionListenerCallback callback,
- NT_Bool immediate_notify) {
- return nt::AddConnectionListener(
- inst,
- [=](const ConnectionNotification& event) {
- NT_ConnectionNotification event_c;
- ConvertToC(event, &event_c);
- callback(data, &event_c);
- DisposeConnectionNotification(&event_c);
- },
- immediate_notify != 0);
-}
-
-NT_ConnectionListenerPoller NT_CreateConnectionListenerPoller(NT_Inst inst) {
- return nt::CreateConnectionListenerPoller(inst);
-}
-
-void NT_DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller) {
- nt::DestroyConnectionListenerPoller(poller);
-}
-
-NT_ConnectionListener NT_AddPolledConnectionListener(
- NT_ConnectionListenerPoller poller, NT_Bool immediate_notify) {
- return nt::AddPolledConnectionListener(poller, immediate_notify);
-}
-
-struct NT_ConnectionNotification* NT_PollConnectionListener(
- NT_ConnectionListenerPoller poller, size_t* len) {
- auto arr_cpp = nt::PollConnectionListener(poller);
- return ConvertToC<NT_ConnectionNotification>(arr_cpp, len);
-}
-
-struct NT_ConnectionNotification* NT_PollConnectionListenerTimeout(
- NT_ConnectionListenerPoller poller, size_t* len, double timeout,
- NT_Bool* timed_out) {
- bool cpp_timed_out = false;
- auto arr_cpp = nt::PollConnectionListener(poller, timeout, &cpp_timed_out);
- *timed_out = cpp_timed_out;
- return ConvertToC<NT_ConnectionNotification>(arr_cpp, len);
-}
-
-void NT_CancelPollConnectionListener(NT_ConnectionListenerPoller poller) {
- nt::CancelPollConnectionListener(poller);
-}
-
-void NT_RemoveConnectionListener(NT_ConnectionListener conn_listener) {
- nt::RemoveConnectionListener(conn_listener);
-}
-
-NT_Bool NT_WaitForConnectionListenerQueue(NT_Inst inst, double timeout) {
- return nt::WaitForConnectionListenerQueue(inst, timeout);
-}
-
-/*
- * Remote Procedure Call Functions
- */
-
-void NT_CreateRpc(NT_Entry entry, const char* def, size_t def_len, void* data,
- NT_RpcCallback callback) {
- nt::CreateRpc(entry, {def, def_len}, [=](const RpcAnswer& answer) {
- NT_RpcAnswer answer_c;
- ConvertToC(answer, &answer_c);
- callback(data, &answer_c);
- NT_DisposeRpcAnswer(&answer_c);
+NT_Listener NT_AddListenerSingle(NT_Inst inst, const char* prefix,
+ size_t prefix_len, unsigned int mask,
+ void* data, NT_ListenerCallback callback) {
+ std::string_view p{prefix, prefix_len};
+ return nt::AddListener(inst, {{p}}, mask, [=](auto& event) {
+ NT_Event event_c;
+ ConvertToC(event, &event_c);
+ callback(data, &event_c);
+ DisposeEvent(&event_c);
});
}
-NT_RpcCallPoller NT_CreateRpcCallPoller(NT_Inst inst) {
- return nt::CreateRpcCallPoller(inst);
-}
-
-void NT_DestroyRpcCallPoller(NT_RpcCallPoller poller) {
- nt::DestroyRpcCallPoller(poller);
-}
-
-void NT_CreatePolledRpc(NT_Entry entry, const char* def, size_t def_len,
- NT_RpcCallPoller poller) {
- nt::CreatePolledRpc(entry, {def, def_len}, poller);
-}
-
-NT_RpcAnswer* NT_PollRpc(NT_RpcCallPoller poller, size_t* len) {
- auto arr_cpp = nt::PollRpc(poller);
- return ConvertToC<NT_RpcAnswer>(arr_cpp, len);
-}
-
-NT_RpcAnswer* NT_PollRpcTimeout(NT_RpcCallPoller poller, size_t* len,
- double timeout, NT_Bool* timed_out) {
- bool cpp_timed_out = false;
- auto arr_cpp = nt::PollRpc(poller, timeout, &cpp_timed_out);
- *timed_out = cpp_timed_out;
- return ConvertToC<NT_RpcAnswer>(arr_cpp, len);
-}
-
-void NT_CancelPollRpc(NT_RpcCallPoller poller) {
- nt::CancelPollRpc(poller);
-}
-
-NT_Bool NT_WaitForRpcCallQueue(NT_Inst inst, double timeout) {
- return nt::WaitForRpcCallQueue(inst, timeout);
-}
-
-NT_Bool NT_PostRpcResponse(NT_Entry entry, NT_RpcCall call, const char* result,
- size_t result_len) {
- return nt::PostRpcResponse(entry, call, {result, result_len});
-}
-
-NT_RpcCall NT_CallRpc(NT_Entry entry, const char* params, size_t params_len) {
- return nt::CallRpc(entry, {params, params_len});
-}
-
-char* NT_GetRpcResult(NT_Entry entry, NT_RpcCall call, size_t* result_len) {
- std::string result;
- if (!nt::GetRpcResult(entry, call, &result)) {
- return nullptr;
+NT_Listener NT_AddListenerMultiple(NT_Inst inst, const NT_String* prefixes,
+ size_t prefixes_len, unsigned int mask,
+ void* data, NT_ListenerCallback callback) {
+ wpi::SmallVector<std::string_view, 8> p;
+ p.reserve(prefixes_len);
+ for (size_t i = 0; i < prefixes_len; ++i) {
+ p.emplace_back(prefixes[i].str, prefixes[i].len);
}
-
- // convert result
- *result_len = result.size();
- char* result_cstr;
- ConvertToC(result, &result_cstr);
- return result_cstr;
+ return nt::AddListener(inst, p, mask, [=](auto& event) {
+ NT_Event event_c;
+ ConvertToC(event, &event_c);
+ callback(data, &event_c);
+ DisposeEvent(&event_c);
+ });
}
-char* NT_GetRpcResultTimeout(NT_Entry entry, NT_RpcCall call,
- size_t* result_len, double timeout,
- NT_Bool* timed_out) {
- std::string result;
- bool cpp_timed_out = false;
- if (!nt::GetRpcResult(entry, call, &result, timeout, &cpp_timed_out)) {
- *timed_out = cpp_timed_out;
- return nullptr;
+NT_Listener NT_AddListener(NT_Topic topic, unsigned int mask, void* data,
+ NT_ListenerCallback callback) {
+ return nt::AddListener(topic, mask, [=](auto& event) {
+ NT_Event event_c;
+ ConvertToC(event, &event_c);
+ callback(data, &event_c);
+ DisposeEvent(&event_c);
+ });
+}
+
+NT_Listener NT_AddPolledListenerSingle(NT_ListenerPoller poller,
+ const char* prefix, size_t prefix_len,
+ unsigned int mask) {
+ std::string_view p{prefix, prefix_len};
+ return nt::AddPolledListener(poller, {{p}}, mask);
+}
+
+NT_Listener NT_AddPolledListenerMultiple(NT_ListenerPoller poller,
+ const NT_String* prefixes,
+ size_t prefixes_len,
+ unsigned int mask) {
+ wpi::SmallVector<std::string_view, 8> p;
+ p.reserve(prefixes_len);
+ for (size_t i = 0; i < prefixes_len; ++i) {
+ p.emplace_back(prefixes[i].str, prefixes[i].len);
}
-
- *timed_out = cpp_timed_out;
- // convert result
- *result_len = result.size();
- char* result_cstr;
- ConvertToC(result, &result_cstr);
- return result_cstr;
+ return nt::AddPolledListener(poller, p, mask);
}
-void NT_CancelRpcResult(NT_Entry entry, NT_RpcCall call) {
- nt::CancelRpcResult(entry, call);
-}
-
-char* NT_PackRpcDefinition(const NT_RpcDefinition* def, size_t* packed_len) {
- auto packed = nt::PackRpcDefinition(ConvertFromC(*def));
-
- // convert result
- *packed_len = packed.size();
- char* packed_cstr;
- ConvertToC(packed, &packed_cstr);
- return packed_cstr;
-}
-
-NT_Bool NT_UnpackRpcDefinition(const char* packed, size_t packed_len,
- NT_RpcDefinition* def) {
- nt::RpcDefinition def_v;
- if (!nt::UnpackRpcDefinition({packed, packed_len}, &def_v)) {
- return 0;
- }
-
- // convert result
- ConvertToC(def_v, def);
- return 1;
-}
-
-char* NT_PackRpcValues(const NT_Value** values, size_t values_len,
- size_t* packed_len) {
- // create input vector
- std::vector<std::shared_ptr<Value>> values_v;
- values_v.reserve(values_len);
- for (size_t i = 0; i < values_len; ++i) {
- values_v.push_back(ConvertFromC(*values[i]));
- }
-
- // make the call
- auto packed = nt::PackRpcValues(values_v);
-
- // convert result
- *packed_len = packed.size();
- char* packed_cstr;
- ConvertToC(packed, &packed_cstr);
- return packed_cstr;
-}
-
-NT_Value** NT_UnpackRpcValues(const char* packed, size_t packed_len,
- const NT_Type* types, size_t types_len) {
- auto values_v = nt::UnpackRpcValues({packed, packed_len}, {types, types_len});
- if (values_v.size() == 0) {
- return nullptr;
- }
-
- // create array and copy into it
- NT_Value** values = static_cast<NT_Value**>(
- wpi::safe_malloc(values_v.size() * sizeof(NT_Value*))); // NOLINT
- for (size_t i = 0; i < values_v.size(); ++i) {
- values[i] = static_cast<NT_Value*>(wpi::safe_malloc(sizeof(NT_Value)));
- ConvertToC(*values_v[i], values[i]);
- }
- return values;
+NT_Listener NT_AddPolledListener(NT_ListenerPoller poller, NT_Topic topic,
+ unsigned int mask) {
+ return nt::AddPolledListener(poller, topic, mask);
}
/*
* Client/Server Functions
*/
-void NT_SetNetworkIdentity(NT_Inst inst, const char* name, size_t name_len) {
- nt::SetNetworkIdentity(inst, {name, name_len});
-}
-
unsigned int NT_GetNetworkMode(NT_Inst inst) {
return nt::GetNetworkMode(inst);
}
@@ -587,34 +498,21 @@
}
void NT_StartServer(NT_Inst inst, const char* persist_filename,
- const char* listen_address, unsigned int port) {
- nt::StartServer(inst, persist_filename, listen_address, port);
+ const char* listen_address, unsigned int port3,
+ unsigned int port4) {
+ nt::StartServer(inst, persist_filename, listen_address, port3, port4);
}
void NT_StopServer(NT_Inst inst) {
nt::StopServer(inst);
}
-void NT_StartClientNone(NT_Inst inst) {
- nt::StartClient(inst);
+void NT_StartClient3(NT_Inst inst, const char* identity) {
+ nt::StartClient3(inst, identity);
}
-void NT_StartClient(NT_Inst inst, const char* server_name, unsigned int port) {
- nt::StartClient(inst, server_name, port);
-}
-
-void NT_StartClientMulti(NT_Inst inst, size_t count, const char** server_names,
- const unsigned int* ports) {
- std::vector<std::pair<std::string_view, unsigned int>> servers;
- servers.reserve(count);
- for (size_t i = 0; i < count; ++i) {
- servers.emplace_back(std::make_pair(server_names[i], ports[i]));
- }
- nt::StartClient(inst, servers);
-}
-
-void NT_StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port) {
- nt::StartClientTeam(inst, team, port);
+void NT_StartClient4(NT_Inst inst, const char* identity) {
+ nt::StartClient4(inst, identity);
}
void NT_StopClient(NT_Inst inst) {
@@ -647,8 +545,8 @@
nt::StopDSClient(inst);
}
-void NT_SetUpdateRate(NT_Inst inst, double interval) {
- nt::SetUpdateRate(inst, interval);
+void NT_FlushLocal(NT_Inst inst) {
+ nt::FlushLocal(inst);
}
void NT_Flush(NT_Inst inst) {
@@ -664,103 +562,67 @@
return ConvertToC<NT_ConnectionInfo>(conn_v, count);
}
-/*
- * File Save/Load Functions
- */
-
-const char* NT_SavePersistent(NT_Inst inst, const char* filename) {
- return nt::SavePersistent(inst, filename);
-}
-
-const char* NT_LoadPersistent(NT_Inst inst, const char* filename,
- void (*warn)(size_t line, const char* msg)) {
- return nt::LoadPersistent(inst, filename, warn);
-}
-
-const char* NT_SaveEntries(NT_Inst inst, const char* filename,
- const char* prefix, size_t prefix_len) {
- return nt::SaveEntries(inst, filename, {prefix, prefix_len});
-}
-
-const char* NT_LoadEntries(NT_Inst inst, const char* filename,
- const char* prefix, size_t prefix_len,
- void (*warn)(size_t line, const char* msg)) {
- return nt::LoadEntries(inst, filename, {prefix, prefix_len}, warn);
+int64_t NT_GetServerTimeOffset(NT_Inst inst, NT_Bool* valid) {
+ if (auto v = nt::GetServerTimeOffset(inst)) {
+ *valid = true;
+ return *v;
+ } else {
+ *valid = false;
+ return 0;
+ }
}
/*
* Utility Functions
*/
-uint64_t NT_Now(void) {
- return wpi::Now();
+int64_t NT_Now(void) {
+ return nt::Now();
}
-NT_Logger NT_AddLogger(NT_Inst inst, void* data, NT_LogFunc func,
- unsigned int min_level, unsigned int max_level) {
- return nt::AddLogger(
- inst,
- [=](const LogMessage& msg) {
- NT_LogMessage msg_c;
- ConvertToC(msg, &msg_c);
- func(data, &msg_c);
- NT_DisposeLogMessage(&msg_c);
- },
- min_level, max_level);
+void NT_SetNow(int64_t timestamp) {
+ nt::SetNow(timestamp);
}
-NT_LoggerPoller NT_CreateLoggerPoller(NT_Inst inst) {
- return nt::CreateLoggerPoller(inst);
+NT_Listener NT_AddLogger(NT_Inst inst, unsigned int min_level,
+ unsigned int max_level, void* data,
+ NT_ListenerCallback func) {
+ return nt::AddLogger(inst, min_level, max_level, [=](auto& event) {
+ NT_Event event_c;
+ ConvertToC(event, &event_c);
+ func(data, &event_c);
+ NT_DisposeEvent(&event_c);
+ });
}
-void NT_DestroyLoggerPoller(NT_LoggerPoller poller) {
- nt::DestroyLoggerPoller(poller);
-}
-
-NT_Logger NT_AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
- unsigned int max_level) {
+NT_Listener NT_AddPolledLogger(NT_ListenerPoller poller, unsigned int min_level,
+ unsigned int max_level) {
return nt::AddPolledLogger(poller, min_level, max_level);
}
-struct NT_LogMessage* NT_PollLogger(NT_LoggerPoller poller, size_t* len) {
- auto arr_cpp = nt::PollLogger(poller);
- return ConvertToC<NT_LogMessage>(arr_cpp, len);
-}
-
-struct NT_LogMessage* NT_PollLoggerTimeout(NT_LoggerPoller poller, size_t* len,
- double timeout, NT_Bool* timed_out) {
- bool cpp_timed_out = false;
- auto arr_cpp = nt::PollLogger(poller, timeout, &cpp_timed_out);
- *timed_out = cpp_timed_out;
- return ConvertToC<NT_LogMessage>(arr_cpp, len);
-}
-
-void NT_CancelPollLogger(NT_LoggerPoller poller) {
- nt::CancelPollLogger(poller);
-}
-
-void NT_RemoveLogger(NT_Logger logger) {
- nt::RemoveLogger(logger);
-}
-
-NT_Bool NT_WaitForLoggerQueue(NT_Inst inst, double timeout) {
- return nt::WaitForLoggerQueue(inst, timeout);
-}
-
void NT_DisposeValue(NT_Value* value) {
switch (value->type) {
case NT_UNASSIGNED:
case NT_BOOLEAN:
+ case NT_INTEGER:
+ case NT_FLOAT:
case NT_DOUBLE:
break;
case NT_STRING:
- case NT_RAW:
- case NT_RPC:
std::free(value->data.v_string.str);
break;
+ case NT_RAW:
+ std::free(value->data.v_raw.data);
+ break;
case NT_BOOLEAN_ARRAY:
std::free(value->data.arr_boolean.arr);
break;
+ case NT_INTEGER_ARRAY:
+ std::free(value->data.arr_int.arr);
+ break;
+ case NT_FLOAT_ARRAY:
+ std::free(value->data.arr_float.arr);
+ break;
case NT_DOUBLE_ARRAY:
std::free(value->data.arr_double.arr);
break;
@@ -776,11 +638,13 @@
}
value->type = NT_UNASSIGNED;
value->last_change = 0;
+ value->server_time = 0;
}
void NT_InitValue(NT_Value* value) {
value->type = NT_UNASSIGNED;
value->last_change = 0;
+ value->server_time = 0;
}
void NT_DisposeString(NT_String* str) {
@@ -794,7 +658,10 @@
str->len = 0;
}
-void NT_DisposeEntryArray(NT_Entry* arr, size_t /*count*/) {
+void NT_DisposeValueArray(struct NT_Value* arr, size_t count) {
+ for (size_t i = 0; i < count; ++i) {
+ NT_DisposeValue(&arr[i]);
+ }
std::free(arr);
}
@@ -805,108 +672,59 @@
std::free(arr);
}
-void NT_DisposeEntryInfoArray(NT_EntryInfo* arr, size_t count) {
+void NT_DisposeTopicInfoArray(NT_TopicInfo* arr, size_t count) {
for (size_t i = 0; i < count; i++) {
- DisposeEntryInfo(&arr[i]);
+ DisposeTopicInfo(&arr[i]);
}
std::free(arr);
}
-void NT_DisposeEntryInfo(NT_EntryInfo* info) {
- DisposeEntryInfo(info);
+void NT_DisposeTopicInfo(NT_TopicInfo* info) {
+ DisposeTopicInfo(info);
}
-void NT_DisposeEntryNotificationArray(NT_EntryNotification* arr, size_t count) {
+void NT_DisposeEventArray(NT_Event* arr, size_t count) {
for (size_t i = 0; i < count; i++) {
- DisposeEntryNotification(&arr[i]);
+ DisposeEvent(&arr[i]);
}
std::free(arr);
}
-void NT_DisposeEntryNotification(NT_EntryNotification* info) {
- DisposeEntryNotification(info);
-}
-
-void NT_DisposeConnectionNotificationArray(NT_ConnectionNotification* arr,
- size_t count) {
- for (size_t i = 0; i < count; i++) {
- DisposeConnectionNotification(&arr[i]);
- }
- std::free(arr);
-}
-
-void NT_DisposeConnectionNotification(NT_ConnectionNotification* info) {
- DisposeConnectionNotification(info);
-}
-
-void NT_DisposeLogMessageArray(NT_LogMessage* arr, size_t count) {
- for (size_t i = 0; i < count; i++) {
- NT_DisposeLogMessage(&arr[i]);
- }
- std::free(arr);
-}
-
-void NT_DisposeLogMessage(NT_LogMessage* info) {
- std::free(info->filename);
- std::free(info->message);
-}
-
-void NT_DisposeRpcDefinition(NT_RpcDefinition* def) {
- NT_DisposeString(&def->name);
-
- for (size_t i = 0; i < def->num_params; ++i) {
- NT_DisposeString(&def->params[i].name);
- NT_DisposeValue(&def->params[i].def_value);
- }
- std::free(def->params);
- def->params = nullptr;
- def->num_params = 0;
-
- for (size_t i = 0; i < def->num_results; ++i) {
- NT_DisposeString(&def->results[i].name);
- }
- std::free(def->results);
- def->results = nullptr;
- def->num_results = 0;
-}
-
-void NT_DisposeRpcAnswerArray(NT_RpcAnswer* arr, size_t count) {
- for (size_t i = 0; i < count; i++) {
- NT_DisposeRpcAnswer(&arr[i]);
- }
- std::free(arr);
-}
-
-void NT_DisposeRpcAnswer(NT_RpcAnswer* call_info) {
- NT_DisposeString(&call_info->name);
- NT_DisposeString(&call_info->params);
- DisposeConnectionInfo(&call_info->conn);
+void NT_DisposeEvent(NT_Event* event) {
+ DisposeEvent(event);
}
/* Interop Utility Functions */
/* Array and Struct Allocations */
-/* Allocates a char array of the specified size.*/
char* NT_AllocateCharArray(size_t size) {
char* retVal = static_cast<char*>(wpi::safe_malloc(size * sizeof(char)));
return retVal;
}
-/* Allocates an integer or boolean array of the specified size. */
int* NT_AllocateBooleanArray(size_t size) {
int* retVal = static_cast<int*>(wpi::safe_malloc(size * sizeof(int)));
return retVal;
}
-/* Allocates a double array of the specified size. */
+int64_t* NT_AllocateIntegerArray(size_t size) {
+ int64_t* retVal =
+ static_cast<int64_t*>(wpi::safe_malloc(size * sizeof(int64_t)));
+ return retVal;
+}
+
+float* NT_AllocateFloatArray(size_t size) {
+ float* retVal = static_cast<float*>(wpi::safe_malloc(size * sizeof(float)));
+ return retVal;
+}
+
double* NT_AllocateDoubleArray(size_t size) {
double* retVal =
static_cast<double*>(wpi::safe_malloc(size * sizeof(double)));
return retVal;
}
-/* Allocates an NT_String array of the specified size. */
struct NT_String* NT_AllocateStringArray(size_t size) {
NT_String* retVal =
static_cast<NT_String*>(wpi::safe_malloc(size * sizeof(NT_String)));
@@ -916,99 +734,25 @@
void NT_FreeCharArray(char* v_char) {
std::free(v_char);
}
-void NT_FreeDoubleArray(double* v_double) {
- std::free(v_double);
-}
void NT_FreeBooleanArray(int* v_boolean) {
std::free(v_boolean);
}
+void NT_FreeIntegerArray(int64_t* v_int) {
+ std::free(v_int);
+}
+void NT_FreeFloatArray(float* v_float) {
+ std::free(v_float);
+}
+void NT_FreeDoubleArray(double* v_double) {
+ std::free(v_double);
+}
void NT_FreeStringArray(struct NT_String* v_string, size_t arr_size) {
- for (size_t i = 0; i < arr_size; i++) {
+ for (size_t i = 0; i < arr_size; ++i) {
std::free(v_string[i].str);
}
std::free(v_string);
}
-NT_Bool NT_SetEntryDouble(NT_Entry entry, uint64_t time, double v_double,
- NT_Bool force) {
- if (force != 0) {
- nt::SetEntryTypeValue(entry, Value::MakeDouble(v_double, time));
- return 1;
- } else {
- return nt::SetEntryValue(entry, Value::MakeDouble(v_double, time));
- }
-}
-
-NT_Bool NT_SetEntryBoolean(NT_Entry entry, uint64_t time, NT_Bool v_boolean,
- NT_Bool force) {
- if (force != 0) {
- nt::SetEntryTypeValue(entry, Value::MakeBoolean(v_boolean != 0, time));
- return 1;
- } else {
- return nt::SetEntryValue(entry, Value::MakeBoolean(v_boolean != 0, time));
- }
-}
-
-NT_Bool NT_SetEntryString(NT_Entry entry, uint64_t time, const char* str,
- size_t str_len, NT_Bool force) {
- if (force != 0) {
- nt::SetEntryTypeValue(entry, Value::MakeString({str, str_len}, time));
- return 1;
- } else {
- return nt::SetEntryValue(entry, Value::MakeString({str, str_len}, time));
- }
-}
-
-NT_Bool NT_SetEntryRaw(NT_Entry entry, uint64_t time, const char* raw,
- size_t raw_len, NT_Bool force) {
- if (force != 0) {
- nt::SetEntryTypeValue(entry, Value::MakeRaw({raw, raw_len}, time));
- return 1;
- } else {
- return nt::SetEntryValue(entry, Value::MakeRaw({raw, raw_len}, time));
- }
-}
-
-NT_Bool NT_SetEntryBooleanArray(NT_Entry entry, uint64_t time,
- const NT_Bool* arr, size_t size,
- NT_Bool force) {
- if (force != 0) {
- nt::SetEntryTypeValue(entry,
- Value::MakeBooleanArray(wpi::span(arr, size), time));
- return 1;
- } else {
- return nt::SetEntryValue(
- entry, Value::MakeBooleanArray(wpi::span(arr, size), time));
- }
-}
-
-NT_Bool NT_SetEntryDoubleArray(NT_Entry entry, uint64_t time, const double* arr,
- size_t size, NT_Bool force) {
- if (force != 0) {
- nt::SetEntryTypeValue(entry, Value::MakeDoubleArray({arr, size}, time));
- return 1;
- } else {
- return nt::SetEntryValue(entry, Value::MakeDoubleArray({arr, size}, time));
- }
-}
-
-NT_Bool NT_SetEntryStringArray(NT_Entry entry, uint64_t time,
- const struct NT_String* arr, size_t size,
- NT_Bool force) {
- std::vector<std::string> v;
- v.reserve(size);
- for (size_t i = 0; i < size; ++i) {
- v.emplace_back(ConvertFromC(arr[i]));
- }
-
- if (force != 0) {
- nt::SetEntryTypeValue(entry, Value::MakeStringArray(std::move(v), time));
- return 1;
- } else {
- return nt::SetEntryValue(entry, Value::MakeStringArray(std::move(v), time));
- }
-}
-
enum NT_Type NT_GetValueType(const struct NT_Value* value) {
if (!value) {
return NT_Type::NT_UNASSIGNED;
@@ -1026,6 +770,26 @@
return 1;
}
+NT_Bool NT_GetValueInteger(const struct NT_Value* value, uint64_t* last_change,
+ int64_t* v_int) {
+ if (!value || value->type != NT_Type::NT_INTEGER) {
+ return 0;
+ }
+ *last_change = value->last_change;
+ *v_int = value->data.v_int;
+ return 1;
+}
+
+NT_Bool NT_GetValueFloat(const struct NT_Value* value, uint64_t* last_change,
+ float* v_float) {
+ if (!value || value->type != NT_Type::NT_FLOAT) {
+ return 0;
+ }
+ *last_change = value->last_change;
+ *v_float = value->data.v_float;
+ return 1;
+}
+
NT_Bool NT_GetValueDouble(const struct NT_Value* value, uint64_t* last_change,
double* v_double) {
if (!value || value->type != NT_Type::NT_DOUBLE) {
@@ -1049,16 +813,16 @@
return str;
}
-char* NT_GetValueRaw(const struct NT_Value* value, uint64_t* last_change,
- size_t* raw_len) {
+uint8_t* NT_GetValueRaw(const struct NT_Value* value, uint64_t* last_change,
+ size_t* raw_len) {
if (!value || value->type != NT_Type::NT_RAW) {
return nullptr;
}
*last_change = value->last_change;
- *raw_len = value->data.v_string.len;
- char* raw =
- static_cast<char*>(wpi::safe_malloc(value->data.v_string.len + 1));
- std::memcpy(raw, value->data.v_string.str, value->data.v_string.len + 1);
+ *raw_len = value->data.v_raw.size;
+ uint8_t* raw =
+ static_cast<uint8_t*>(wpi::safe_malloc(value->data.v_raw.size));
+ std::memcpy(raw, value->data.v_raw.data, value->data.v_raw.size);
return raw;
}
@@ -1076,6 +840,34 @@
return arr;
}
+int64_t* NT_GetValueIntegerArray(const struct NT_Value* value,
+ uint64_t* last_change, size_t* arr_size) {
+ if (!value || value->type != NT_Type::NT_INTEGER_ARRAY) {
+ return nullptr;
+ }
+ *last_change = value->last_change;
+ *arr_size = value->data.arr_int.size;
+ int64_t* arr = static_cast<int64_t*>(
+ wpi::safe_malloc(value->data.arr_int.size * sizeof(int64_t)));
+ std::memcpy(arr, value->data.arr_int.arr,
+ value->data.arr_int.size * sizeof(int64_t));
+ return arr;
+}
+
+float* NT_GetValueFloatArray(const struct NT_Value* value,
+ uint64_t* last_change, size_t* arr_size) {
+ if (!value || value->type != NT_Type::NT_FLOAT_ARRAY) {
+ return nullptr;
+ }
+ *last_change = value->last_change;
+ *arr_size = value->data.arr_float.size;
+ float* arr = static_cast<float*>(
+ wpi::safe_malloc(value->data.arr_float.size * sizeof(float)));
+ std::memcpy(arr, value->data.arr_float.arr,
+ value->data.arr_float.size * sizeof(float));
+ return arr;
+}
+
double* NT_GetValueDoubleArray(const struct NT_Value* value,
uint64_t* last_change, size_t* arr_size) {
if (!value || value->type != NT_Type::NT_DOUBLE_ARRAY) {
@@ -1108,151 +900,4 @@
return arr;
}
-NT_Bool NT_SetDefaultEntryBoolean(NT_Entry entry, uint64_t time,
- NT_Bool default_boolean) {
- return nt::SetDefaultEntryValue(
- entry, Value::MakeBoolean(default_boolean != 0, time));
-}
-
-NT_Bool NT_SetDefaultEntryDouble(NT_Entry entry, uint64_t time,
- double default_double) {
- return nt::SetDefaultEntryValue(entry,
- Value::MakeDouble(default_double, time));
-}
-
-NT_Bool NT_SetDefaultEntryString(NT_Entry entry, uint64_t time,
- const char* default_value,
- size_t default_len) {
- return nt::SetDefaultEntryValue(
- entry, Value::MakeString({default_value, default_len}, time));
-}
-
-NT_Bool NT_SetDefaultEntryRaw(NT_Entry entry, uint64_t time,
- const char* default_value, size_t default_len) {
- return nt::SetDefaultEntryValue(
- entry, Value::MakeRaw({default_value, default_len}, time));
-}
-
-NT_Bool NT_SetDefaultEntryBooleanArray(NT_Entry entry, uint64_t time,
- const NT_Bool* default_value,
- size_t default_size) {
- return nt::SetDefaultEntryValue(
- entry,
- Value::MakeBooleanArray(wpi::span(default_value, default_size), time));
-}
-
-NT_Bool NT_SetDefaultEntryDoubleArray(NT_Entry entry, uint64_t time,
- const double* default_value,
- size_t default_size) {
- return nt::SetDefaultEntryValue(
- entry, Value::MakeDoubleArray({default_value, default_size}, time));
-}
-
-NT_Bool NT_SetDefaultEntryStringArray(NT_Entry entry, uint64_t time,
- const struct NT_String* default_value,
- size_t default_size) {
- std::vector<std::string> vec;
- vec.reserve(default_size);
- for (size_t i = 0; i < default_size; ++i) {
- vec.emplace_back(ConvertFromC(default_value[i]));
- }
-
- return nt::SetDefaultEntryValue(entry,
- Value::MakeStringArray(std::move(vec), time));
-}
-
-NT_Bool NT_GetEntryBoolean(NT_Entry entry, uint64_t* last_change,
- NT_Bool* v_boolean) {
- auto v = nt::GetEntryValue(entry);
- if (!v || !v->IsBoolean()) {
- return 0;
- }
- *v_boolean = v->GetBoolean();
- *last_change = v->last_change();
- return 1;
-}
-
-NT_Bool NT_GetEntryDouble(NT_Entry entry, uint64_t* last_change,
- double* v_double) {
- auto v = nt::GetEntryValue(entry);
- if (!v || !v->IsDouble()) {
- return 0;
- }
- *last_change = v->last_change();
- *v_double = v->GetDouble();
- return 1;
-}
-
-char* NT_GetEntryString(NT_Entry entry, uint64_t* last_change,
- size_t* str_len) {
- auto v = nt::GetEntryValue(entry);
- if (!v || !v->IsString()) {
- return nullptr;
- }
- *last_change = v->last_change();
- struct NT_String v_string;
- nt::ConvertToC(v->GetString(), &v_string);
- *str_len = v_string.len;
- return v_string.str;
-}
-
-char* NT_GetEntryRaw(NT_Entry entry, uint64_t* last_change, size_t* raw_len) {
- auto v = nt::GetEntryValue(entry);
- if (!v || !v->IsRaw()) {
- return nullptr;
- }
- *last_change = v->last_change();
- struct NT_String v_raw;
- nt::ConvertToC(v->GetRaw(), &v_raw);
- *raw_len = v_raw.len;
- return v_raw.str;
-}
-
-NT_Bool* NT_GetEntryBooleanArray(NT_Entry entry, uint64_t* last_change,
- size_t* arr_size) {
- auto v = nt::GetEntryValue(entry);
- if (!v || !v->IsBooleanArray()) {
- return nullptr;
- }
- *last_change = v->last_change();
- auto vArr = v->GetBooleanArray();
- NT_Bool* arr =
- static_cast<int*>(wpi::safe_malloc(vArr.size() * sizeof(NT_Bool)));
- *arr_size = vArr.size();
- std::copy(vArr.begin(), vArr.end(), arr);
- return arr;
-}
-
-double* NT_GetEntryDoubleArray(NT_Entry entry, uint64_t* last_change,
- size_t* arr_size) {
- auto v = nt::GetEntryValue(entry);
- if (!v || !v->IsDoubleArray()) {
- return nullptr;
- }
- *last_change = v->last_change();
- auto vArr = v->GetDoubleArray();
- double* arr =
- static_cast<double*>(wpi::safe_malloc(vArr.size() * sizeof(double)));
- *arr_size = vArr.size();
- std::copy(vArr.begin(), vArr.end(), arr);
- return arr;
-}
-
-NT_String* NT_GetEntryStringArray(NT_Entry entry, uint64_t* last_change,
- size_t* arr_size) {
- auto v = nt::GetEntryValue(entry);
- if (!v || !v->IsStringArray()) {
- return nullptr;
- }
- *last_change = v->last_change();
- auto vArr = v->GetStringArray();
- NT_String* arr = static_cast<NT_String*>(
- wpi::safe_malloc(vArr.size() * sizeof(NT_String)));
- for (size_t i = 0; i < vArr.size(); ++i) {
- ConvertToC(vArr[i], &arr[i]);
- }
- *arr_size = vArr.size();
- return arr;
-}
-
} // extern "C"
diff --git a/ntcore/src/main/native/cpp/ntcore_cpp.cpp b/ntcore/src/main/native/cpp/ntcore_cpp.cpp
index 8616aaf..cfb5af2 100644
--- a/ntcore/src/main/native/cpp/ntcore_cpp.cpp
+++ b/ntcore/src/main/native/cpp/ntcore_cpp.cpp
@@ -4,21 +4,35 @@
#include <stdint.h>
+#include <atomic>
#include <cassert>
#include <cstdio>
#include <cstdlib>
+#include <fmt/format.h>
+#include <wpi/json.h>
#include <wpi/timestamp.h>
#include "Handle.h"
#include "InstanceImpl.h"
#include "Log.h"
-#include "WireDecoder.h"
-#include "WireEncoder.h"
+#include "Types_internal.h"
#include "ntcore.h"
+#include "ntcore_c.h"
+
+static std::atomic_bool gNowSet{false};
+static std::atomic<int64_t> gNowTime;
namespace nt {
+wpi::json TopicInfo::GetProperties() const {
+ try {
+ return wpi::json::parse(properties);
+ } catch (wpi::json::parse_error&) {
+ return wpi::json::object();
+ }
+}
+
/*
* Instance Functions
*/
@@ -31,6 +45,12 @@
return Handle{InstanceImpl::Alloc(), 0, Handle::kInstance};
}
+void ResetInstance(NT_Inst inst) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->Reset();
+ }
+}
+
void DestroyInstance(NT_Inst inst) {
int i = Handle{inst}.GetTypedInst(Handle::kInstance);
if (i < 0) {
@@ -42,7 +62,7 @@
NT_Inst GetInstanceFromHandle(NT_Handle handle) {
Handle h{handle};
auto type = h.GetType();
- if (type >= Handle::kConnectionListener && type <= Handle::kRpcCallPoller) {
+ if (type >= Handle::kListener && type < Handle::kTypeMax) {
return Handle(h.GetInst(), 0, Handle::kInstance);
}
@@ -54,1120 +74,704 @@
*/
NT_Entry GetEntry(NT_Inst inst, std::string_view name) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return 0;
- }
-
- unsigned int id = ii->storage.GetEntry(name);
- if (id == UINT_MAX) {
- return 0;
- }
- return Handle(i, id, Handle::kEntry);
-}
-
-std::vector<NT_Entry> GetEntries(NT_Inst inst, std::string_view prefix,
- unsigned int types) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.GetEntry(name);
+ } else {
return {};
}
-
- auto arr = ii->storage.GetEntries(prefix, types);
- // convert indices to handles
- for (auto& val : arr) {
- val = Handle(i, val, Handle::kEntry);
- }
- return arr;
}
std::string GetEntryName(NT_Entry entry) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
+ if (auto ii = InstanceImpl::GetHandle(entry)) {
+ return ii->localStorage.GetEntryName(entry);
+ } else {
return {};
}
-
- return ii->storage.GetEntryName(id);
}
NT_Type GetEntryType(NT_Entry entry) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return NT_UNASSIGNED;
+ if (auto ii = InstanceImpl::GetHandle(entry)) {
+ return ii->localStorage.GetEntryType(entry);
+ } else {
+ return {};
}
-
- return ii->storage.GetEntryType(id);
}
-uint64_t GetEntryLastChange(NT_Entry entry) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return 0;
+int64_t GetEntryLastChange(NT_Handle subentry) {
+ if (auto ii = InstanceImpl::GetHandle(subentry)) {
+ return ii->localStorage.GetEntryLastChange(subentry);
+ } else {
+ return {};
}
-
- return ii->storage.GetEntryLastChange(id);
}
-std::shared_ptr<Value> GetEntryValue(NT_Entry entry) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return nullptr;
+Value GetEntryValue(NT_Handle subentry) {
+ if (auto ii = InstanceImpl::GetHandle(subentry)) {
+ return ii->localStorage.GetEntryValue(subentry);
+ } else {
+ return {};
}
-
- return ii->storage.GetEntryValue(id);
}
-bool SetDefaultEntryValue(NT_Entry entry, std::shared_ptr<Value> value) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return false;
+bool SetDefaultEntryValue(NT_Entry entry, const Value& value) {
+ if (auto ii = InstanceImpl::GetHandle(entry)) {
+ return ii->localStorage.SetDefaultEntryValue(entry, value);
+ } else {
+ return {};
}
-
- return ii->storage.SetDefaultEntryValue(id, value);
}
-bool SetEntryValue(NT_Entry entry, std::shared_ptr<Value> value) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return false;
+bool SetEntryValue(NT_Entry entry, const Value& value) {
+ if (auto ii = InstanceImpl::GetHandle(entry)) {
+ return ii->localStorage.SetEntryValue(entry, value);
+ } else {
+ return {};
}
-
- return ii->storage.SetEntryValue(id, value);
-}
-
-void SetEntryTypeValue(NT_Entry entry, std::shared_ptr<Value> value) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- ii->storage.SetEntryTypeValue(id, value);
}
void SetEntryFlags(NT_Entry entry, unsigned int flags) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
+ if (auto ii = InstanceImpl::GetHandle(entry)) {
+ ii->localStorage.SetEntryFlags(entry, flags);
}
-
- ii->storage.SetEntryFlags(id, flags);
}
unsigned int GetEntryFlags(NT_Entry entry) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return 0;
- }
-
- return ii->storage.GetEntryFlags(id);
-}
-
-void DeleteEntry(NT_Entry entry) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- ii->storage.DeleteEntry(id);
-}
-
-void DeleteAllEntries(NT_Inst inst) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (i < 0 || !ii) {
- return;
- }
-
- ii->storage.DeleteAllEntries();
-}
-
-std::vector<EntryInfo> GetEntryInfo(NT_Inst inst, std::string_view prefix,
- unsigned int types) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
+ if (auto ii = InstanceImpl::GetHandle(entry)) {
+ return ii->localStorage.GetEntryFlags(entry);
+ } else {
return {};
}
-
- return ii->storage.GetEntryInfo(i, prefix, types);
}
-EntryInfo GetEntryInfo(NT_Entry entry) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- int i = handle.GetInst();
- auto ii = InstanceImpl::Get(i);
- if (id < 0 || !ii) {
- EntryInfo info;
- info.entry = 0;
- info.type = NT_UNASSIGNED;
- info.flags = 0;
- info.last_change = 0;
- return info;
+std::vector<Value> ReadQueueValue(NT_Handle subentry) {
+ if (auto ii = InstanceImpl::GetHandle(subentry)) {
+ return ii->localStorage.ReadQueueValue(subentry);
+ } else {
+ return {};
}
+}
- return ii->storage.GetEntryInfo(i, id);
+/*
+ * Topic Functions
+ */
+
+std::vector<NT_Topic> GetTopics(NT_Inst inst, std::string_view prefix,
+ unsigned int types) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.GetTopics(prefix, types);
+ } else {
+ return {};
+ }
+}
+
+std::vector<NT_Topic> GetTopics(NT_Inst inst, std::string_view prefix,
+ std::span<const std::string_view> types) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.GetTopics(prefix, types);
+ } else {
+ return {};
+ }
+}
+
+std::vector<TopicInfo> GetTopicInfo(NT_Inst inst, std::string_view prefix,
+ unsigned int types) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.GetTopicInfo(prefix, types);
+ } else {
+ return {};
+ }
+}
+
+std::vector<TopicInfo> GetTopicInfo(NT_Inst inst, std::string_view prefix,
+ std::span<const std::string_view> types) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.GetTopicInfo(prefix, types);
+ } else {
+ return {};
+ }
+}
+
+TopicInfo GetTopicInfo(NT_Topic topic) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicInfo(topic);
+ } else {
+ return {};
+ }
+}
+
+NT_Topic GetTopic(NT_Inst inst, std::string_view name) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.GetTopic(name);
+ } else {
+ return {};
+ }
+}
+
+std::string GetTopicName(NT_Topic topic) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicName(topic);
+ } else {
+ return {};
+ }
+}
+
+NT_Type GetTopicType(NT_Topic topic) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicType(topic);
+ } else {
+ return {};
+ }
+}
+
+std::string GetTopicTypeString(NT_Topic topic) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicTypeString(topic);
+ } else {
+ return {};
+ }
+}
+
+void SetTopicPersistent(NT_Topic topic, bool value) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ ii->localStorage.SetTopicPersistent(topic, value);
+ } else {
+ return;
+ }
+}
+
+bool GetTopicPersistent(NT_Topic topic) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicPersistent(topic);
+ } else {
+ return {};
+ }
+}
+
+void SetTopicRetained(NT_Topic topic, bool value) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ ii->localStorage.SetTopicRetained(topic, value);
+ } else {
+ return;
+ }
+}
+
+bool GetTopicRetained(NT_Topic topic) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicRetained(topic);
+ } else {
+ return {};
+ }
+}
+
+bool GetTopicExists(NT_Handle handle) {
+ if (auto ii = InstanceImpl::GetHandle(handle)) {
+ return ii->localStorage.GetTopicExists(handle);
+ }
+ return false;
+}
+
+wpi::json GetTopicProperty(NT_Topic topic, std::string_view name) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicProperty(topic, name);
+ } else {
+ return {};
+ }
+}
+
+void SetTopicProperty(NT_Topic topic, std::string_view name,
+ const wpi::json& value) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ ii->localStorage.SetTopicProperty(topic, name, value);
+ } else {
+ return;
+ }
+}
+
+void DeleteTopicProperty(NT_Topic topic, std::string_view name) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ ii->localStorage.DeleteTopicProperty(topic, name);
+ }
+}
+
+wpi::json GetTopicProperties(NT_Topic topic) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetTopicProperties(topic);
+ } else {
+ return {};
+ }
+}
+
+bool SetTopicProperties(NT_Topic topic, const wpi::json& properties) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.SetTopicProperties(topic, properties);
+ } else {
+ return {};
+ }
+}
+
+NT_Subscriber Subscribe(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.Subscribe(topic, type, typeStr, options);
+ } else {
+ return {};
+ }
+}
+
+void Unsubscribe(NT_Subscriber sub) {
+ if (auto ii = InstanceImpl::GetTyped(sub, Handle::kSubscriber)) {
+ ii->localStorage.Unsubscribe(sub);
+ }
+}
+
+NT_Publisher Publish(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options) {
+ return PublishEx(topic, type, typeStr, wpi::json::object(), options);
+}
+
+NT_Publisher PublishEx(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptions& options) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.Publish(topic, type, typeStr, properties, options);
+ } else {
+ return {};
+ }
+}
+
+void Unpublish(NT_Handle pubentry) {
+ if (auto ii = InstanceImpl::GetHandle(pubentry)) {
+ ii->localStorage.Unpublish(pubentry);
+ }
+}
+
+NT_Entry GetEntry(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options) {
+ if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) {
+ return ii->localStorage.GetEntry(topic, type, typeStr, options);
+ } else {
+ return {};
+ }
+}
+
+void ReleaseEntry(NT_Entry entry) {
+ if (auto ii = InstanceImpl::GetTyped(entry, Handle::kEntry)) {
+ ii->localStorage.ReleaseEntry(entry);
+ }
+}
+
+void Release(NT_Handle pubsubentry) {
+ if (auto ii = InstanceImpl::GetHandle(pubsubentry)) {
+ ii->localStorage.Release(pubsubentry);
+ }
+}
+
+NT_Topic GetTopicFromHandle(NT_Handle pubsubentry) {
+ if (auto ii = InstanceImpl::GetHandle(pubsubentry)) {
+ return ii->localStorage.GetTopicFromHandle(pubsubentry);
+ } else {
+ return {};
+ }
+}
+
+NT_MultiSubscriber SubscribeMultiple(NT_Inst inst,
+ std::span<const std::string_view> prefixes,
+ const PubSubOptions& options) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.SubscribeMultiple(prefixes, options);
+ } else {
+ return {};
+ }
+}
+
+void UnsubscribeMultiple(NT_MultiSubscriber sub) {
+ if (auto ii = InstanceImpl::GetTyped(sub, Handle::kMultiSubscriber)) {
+ ii->localStorage.UnsubscribeMultiple(sub);
+ }
}
/*
* Callback Creation Functions
*/
-NT_EntryListener AddEntryListener(
- NT_Inst inst, std::string_view prefix,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (i < 0 || !ii) {
- return 0;
+static void CleanupListeners(
+ InstanceImpl& ii,
+ std::span<const std::pair<NT_Listener, unsigned int>> listeners) {
+ bool updateMinLevel = false;
+ for (auto&& [listener, mask] : listeners) {
+ // connection doesn't need removal notification
+ if ((mask & (NT_EVENT_TOPIC | NT_EVENT_VALUE_ALL)) != 0) {
+ ii.localStorage.RemoveListener(listener, mask);
+ }
+ if ((mask & NT_EVENT_LOGMESSAGE) != 0) {
+ ii.logger_impl.RemoveListener(listener);
+ updateMinLevel = true;
+ }
}
-
- unsigned int uid = ii->storage.AddListener(prefix, callback, flags);
- return Handle(i, uid, Handle::kEntryListener);
+ if (updateMinLevel) {
+ ii.logger.set_min_level(ii.logger_impl.GetMinLevel());
+ }
}
-NT_EntryListener AddEntryListener(
- NT_Entry entry,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- int i = handle.GetInst();
- auto ii = InstanceImpl::Get(i);
- if (id < 0 || !ii) {
- return 0;
+static void DoAddListener(InstanceImpl& ii, NT_Listener listener,
+ NT_Handle handle, unsigned int mask) {
+ if (Handle{handle}.IsType(Handle::kInstance)) {
+ if ((mask & NT_EVENT_CONNECTION) != 0) {
+ ii.connectionList.AddListener(listener, mask);
+ }
+ if ((mask & NT_EVENT_LOGMESSAGE) != 0) {
+ ii.logger_impl.AddListener(listener, NT_LOG_INFO, UINT_MAX);
+ }
+ if ((mask & NT_EVENT_TIMESYNC) != 0) {
+ ii.AddTimeSyncListener(listener, mask);
+ }
+ } else if ((mask & (NT_EVENT_TOPIC | NT_EVENT_VALUE_ALL)) != 0) {
+ ii.localStorage.AddListener(listener, handle, mask);
}
-
- unsigned int uid = ii->storage.AddListener(id, callback, flags);
- return Handle(i, uid, Handle::kEntryListener);
}
-NT_EntryListenerPoller CreateEntryListenerPoller(NT_Inst inst) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return 0;
- }
-
- return Handle(i, ii->entry_notifier.CreatePoller(),
- Handle::kEntryListenerPoller);
-}
-
-void DestroyEntryListenerPoller(NT_EntryListenerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- ii->entry_notifier.RemovePoller(id);
-}
-
-NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
- std::string_view prefix,
- unsigned int flags) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
- int i = handle.GetInst();
- auto ii = InstanceImpl::Get(i);
- if (id < 0 || !ii) {
- return 0;
- }
-
- unsigned int uid = ii->storage.AddPolledListener(id, prefix, flags);
- return Handle(i, uid, Handle::kEntryListener);
-}
-
-NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
- NT_Entry entry, unsigned int flags) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- int i = handle.GetInst();
- auto ii = InstanceImpl::Get(i);
- if (id < 0 || !ii) {
- return 0;
- }
-
- Handle phandle{poller};
- int p_id = phandle.GetTypedIndex(Handle::kEntryListenerPoller);
- if (p_id < 0) {
- return 0;
- }
- if (handle.GetInst() != phandle.GetInst()) {
- return 0;
- }
-
- unsigned int uid = ii->storage.AddPolledListener(p_id, id, flags);
- return Handle(i, uid, Handle::kEntryListener);
-}
-
-std::vector<EntryNotification> PollEntryListener(
- NT_EntryListenerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
+NT_ListenerPoller CreateListenerPoller(NT_Inst inst) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->listenerStorage.CreateListenerPoller();
+ } else {
return {};
}
-
- return ii->entry_notifier.Poll(static_cast<unsigned int>(id));
}
-std::vector<EntryNotification> PollEntryListener(NT_EntryListenerPoller poller,
- double timeout,
- bool* timed_out) {
- *timed_out = false;
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
+void DestroyListenerPoller(NT_ListenerPoller poller) {
+ if (auto ii = InstanceImpl::GetTyped(poller, Handle::kListenerPoller)) {
+ CleanupListeners(*ii, ii->listenerStorage.DestroyListenerPoller(poller));
+ }
+}
+
+std::vector<Event> ReadListenerQueue(NT_ListenerPoller poller) {
+ if (auto ii = InstanceImpl::GetTyped(poller, Handle::kListenerPoller)) {
+ return ii->listenerStorage.ReadListenerQueue(poller);
+ } else {
return {};
}
-
- return ii->entry_notifier.Poll(static_cast<unsigned int>(id), timeout,
- timed_out);
}
-void CancelPollEntryListener(NT_EntryListenerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kEntryListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
+void RemoveListener(NT_Listener listener) {
+ if (auto ii = InstanceImpl::GetTyped(listener, Handle::kListener)) {
+ CleanupListeners(*ii, ii->listenerStorage.RemoveListener(listener));
}
-
- ii->entry_notifier.CancelPoll(id);
}
-void RemoveEntryListener(NT_EntryListener entry_listener) {
- Handle handle{entry_listener};
- int uid = handle.GetTypedIndex(Handle::kEntryListener);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (uid < 0 || !ii) {
- return;
- }
-
- ii->entry_notifier.Remove(uid);
-}
-
-bool WaitForEntryListenerQueue(NT_Inst inst, double timeout) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return true;
- }
- return ii->entry_notifier.WaitForQueue(timeout);
-}
-
-NT_ConnectionListener AddConnectionListener(
- NT_Inst inst,
- std::function<void(const ConnectionNotification& event)> callback,
- bool immediate_notify) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return 0;
- }
-
- unsigned int uid = ii->dispatcher.AddListener(callback, immediate_notify);
- return Handle(i, uid, Handle::kConnectionListener);
-}
-
-NT_ConnectionListenerPoller CreateConnectionListenerPoller(NT_Inst inst) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return 0;
- }
-
- return Handle(i, ii->connection_notifier.CreatePoller(),
- Handle::kConnectionListenerPoller);
-}
-
-void DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- ii->connection_notifier.RemovePoller(id);
-}
-
-NT_ConnectionListener AddPolledConnectionListener(
- NT_ConnectionListenerPoller poller, bool immediate_notify) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
- int i = handle.GetInst();
- auto ii = InstanceImpl::Get(i);
- if (id < 0 || !ii) {
- return 0;
- }
-
- unsigned int uid = ii->dispatcher.AddPolledListener(id, immediate_notify);
- return Handle(i, uid, Handle::kConnectionListener);
-}
-
-std::vector<ConnectionNotification> PollConnectionListener(
- NT_ConnectionListenerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
+bool WaitForListenerQueue(NT_Handle handle, double timeout) {
+ if (auto ii = InstanceImpl::GetHandle(handle)) {
+ return ii->listenerStorage.WaitForListenerQueue(timeout);
+ } else {
return {};
}
-
- return ii->connection_notifier.Poll(static_cast<unsigned int>(id));
}
-std::vector<ConnectionNotification> PollConnectionListener(
- NT_ConnectionListenerPoller poller, double timeout, bool* timed_out) {
- *timed_out = false;
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
+NT_Listener AddListener(NT_Inst inst,
+ std::span<const std::string_view> prefixes,
+ unsigned int mask, ListenerCallback callback) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ if ((mask & (NT_EVENT_TOPIC | NT_EVENT_VALUE_ALL)) != 0) {
+ auto listener = ii->listenerStorage.AddListener(std::move(callback));
+ ii->localStorage.AddListener(listener, prefixes, mask);
+ return listener;
+ }
+ }
+ return {};
+}
+
+NT_Listener AddListener(NT_Handle handle, unsigned int mask,
+ ListenerCallback callback) {
+ if (auto ii = InstanceImpl::GetHandle(handle)) {
+ auto listener = ii->listenerStorage.AddListener(std::move(callback));
+ DoAddListener(*ii, listener, handle, mask);
+ return listener;
+ } else {
return {};
}
-
- return ii->connection_notifier.Poll(static_cast<unsigned int>(id), timeout,
- timed_out);
}
-void CancelPollConnectionListener(NT_ConnectionListenerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
+NT_Listener AddPolledListener(NT_ListenerPoller poller,
+ std::span<const std::string_view> prefixes,
+ unsigned int mask) {
+ if (auto ii = InstanceImpl::GetTyped(poller, Handle::kListenerPoller)) {
+ if ((mask & (NT_EVENT_TOPIC | NT_EVENT_VALUE_ALL)) != 0) {
+ auto listener = ii->listenerStorage.AddListener(poller);
+ ii->localStorage.AddListener(listener, prefixes, mask);
+ return listener;
+ }
}
-
- ii->connection_notifier.CancelPoll(id);
+ return {};
}
-void RemoveConnectionListener(NT_ConnectionListener conn_listener) {
- Handle handle{conn_listener};
- int uid = handle.GetTypedIndex(Handle::kConnectionListener);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (uid < 0 || !ii) {
- return;
+NT_Listener AddPolledListener(NT_ListenerPoller poller, NT_Handle handle,
+ unsigned int mask) {
+ if (auto ii = InstanceImpl::GetTyped(poller, Handle::kListenerPoller)) {
+ if (Handle{handle}.GetInst() != Handle{poller}.GetInst()) {
+ WPI_ERROR(
+ ii->logger,
+ "AddPolledListener(): trying to listen to handle {} (instance {}) "
+ "with poller {} (instance {}), ignored due to different instance",
+ handle, Handle{handle}.GetInst(), poller, Handle{poller}.GetInst());
+ return {};
+ }
+ auto listener = ii->listenerStorage.AddListener(poller);
+ DoAddListener(*ii, listener, handle, mask);
+ return listener;
+ } else {
+ return {};
}
-
- ii->connection_notifier.Remove(uid);
}
-bool WaitForConnectionListenerQueue(NT_Inst inst, double timeout) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return true;
+int64_t Now() {
+ if (gNowSet) {
+ return gNowTime;
}
- return ii->connection_notifier.WaitForQueue(timeout);
+ return wpi::Now();
+}
+
+void SetNow(int64_t timestamp) {
+ gNowTime = timestamp;
+ gNowSet = true;
+}
+
+NT_Type GetTypeFromString(std::string_view typeString) {
+ if (typeString.empty()) {
+ return NT_UNASSIGNED;
+ } else {
+ return StringToType(typeString);
+ }
+}
+
+std::string_view GetStringFromType(NT_Type type) {
+ if (type == NT_UNASSIGNED) {
+ return "";
+ } else {
+ return TypeToString(type);
+ }
}
/*
- * Remote Procedure Call Functions
+ * Data Logger Functions
*/
-
-void CreateRpc(NT_Entry entry, std::string_view def,
- std::function<void(const RpcAnswer& answer)> callback) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- // only server can create RPCs
- if ((ii->dispatcher.GetNetworkMode() & NT_NET_MODE_SERVER) == 0) {
- return;
- }
- if (def.empty() || !callback) {
- return;
- }
-
- ii->storage.CreateRpc(id, def, ii->rpc_server.Add(callback));
-}
-
-NT_RpcCallPoller CreateRpcCallPoller(NT_Inst inst) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
+NT_DataLogger StartEntryDataLog(NT_Inst inst, wpi::log::DataLog& log,
+ std::string_view prefix,
+ std::string_view logPrefix) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->localStorage.StartDataLog(log, prefix, logPrefix);
+ } else {
return 0;
}
-
- return Handle(i, ii->rpc_server.CreatePoller(), Handle::kRpcCallPoller);
}
-void DestroyRpcCallPoller(NT_RpcCallPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
+void StopEntryDataLog(NT_DataLogger logger) {
+ if (auto ii = InstanceImpl::GetTyped(logger, Handle::kDataLogger)) {
+ ii->localStorage.StopDataLog(logger);
}
-
- ii->rpc_server.RemovePoller(id);
}
-void CreatePolledRpc(NT_Entry entry, std::string_view def,
- NT_RpcCallPoller poller) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- Handle phandle{poller};
- int p_id = phandle.GetTypedIndex(Handle::kRpcCallPoller);
- if (p_id < 0) {
- return;
- }
- if (handle.GetInst() != phandle.GetInst()) {
- return;
- }
-
- // only server can create RPCs
- if ((ii->dispatcher.GetNetworkMode() & NT_NET_MODE_SERVER) == 0) {
- return;
- }
- if (def.empty()) {
- return;
- }
-
- ii->storage.CreateRpc(id, def, ii->rpc_server.AddPolled(p_id));
-}
-
-std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return {};
- }
-
- return ii->rpc_server.Poll(static_cast<unsigned int>(id));
-}
-
-std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller, double timeout,
- bool* timed_out) {
- *timed_out = false;
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return {};
- }
-
- return ii->rpc_server.Poll(static_cast<unsigned int>(id), timeout, timed_out);
-}
-
-void CancelPollRpc(NT_RpcCallPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kRpcCallPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- ii->rpc_server.CancelPoll(id);
-}
-
-bool WaitForRpcCallQueue(NT_Inst inst, double timeout) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return true;
- }
- return ii->rpc_server.WaitForQueue(timeout);
-}
-
-bool PostRpcResponse(NT_Entry entry, NT_RpcCall call, std::string_view result) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return false;
- }
-
- Handle chandle{call};
- int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
- if (call_uid < 0) {
- return false;
- }
- if (handle.GetInst() != chandle.GetInst()) {
- return false;
- }
-
- return ii->rpc_server.PostRpcResponse(id, call_uid, result);
-}
-
-NT_RpcCall CallRpc(NT_Entry entry, std::string_view params) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- int i = handle.GetInst();
- auto ii = InstanceImpl::Get(i);
- if (id < 0 || !ii) {
+NT_ConnectionDataLogger StartConnectionDataLog(NT_Inst inst,
+ wpi::log::DataLog& log,
+ std::string_view name) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->connectionList.StartDataLog(log, name);
+ } else {
return 0;
}
-
- unsigned int call_uid = ii->storage.CallRpc(id, params);
- if (call_uid == 0) {
- return 0;
- }
- return Handle(i, call_uid, Handle::kRpcCall);
}
-bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return false;
+void StopConnectionDataLog(NT_ConnectionDataLogger logger) {
+ if (auto ii = InstanceImpl::GetTyped(logger, Handle::kConnectionDataLogger)) {
+ ii->connectionList.StopDataLog(logger);
}
-
- Handle chandle{call};
- int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
- if (call_uid < 0) {
- return false;
- }
- if (handle.GetInst() != chandle.GetInst()) {
- return false;
- }
-
- return ii->storage.GetRpcResult(id, call_uid, result);
-}
-
-bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result,
- double timeout, bool* timed_out) {
- *timed_out = false;
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return false;
- }
-
- Handle chandle{call};
- int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
- if (call_uid < 0) {
- return false;
- }
- if (handle.GetInst() != chandle.GetInst()) {
- return false;
- }
-
- return ii->storage.GetRpcResult(id, call_uid, result, timeout, timed_out);
-}
-
-void CancelRpcResult(NT_Entry entry, NT_RpcCall call) {
- Handle handle{entry};
- int id = handle.GetTypedIndex(Handle::kEntry);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- Handle chandle{call};
- int call_uid = chandle.GetTypedIndex(Handle::kRpcCall);
- if (call_uid < 0) {
- return;
- }
- if (handle.GetInst() != chandle.GetInst()) {
- return;
- }
-
- ii->storage.CancelRpcResult(id, call_uid);
-}
-
-std::string PackRpcDefinition(const RpcDefinition& def) {
- WireEncoder enc(0x0300);
- enc.Write8(def.version);
- enc.WriteString(def.name);
-
- // parameters
- unsigned int params_size = def.params.size();
- if (params_size > 0xff) {
- params_size = 0xff;
- }
- enc.Write8(params_size);
- for (size_t i = 0; i < params_size; ++i) {
- enc.WriteType(def.params[i].def_value->type());
- enc.WriteString(def.params[i].name);
- enc.WriteValue(*def.params[i].def_value);
- }
-
- // results
- unsigned int results_size = def.results.size();
- if (results_size > 0xff) {
- results_size = 0xff;
- }
- enc.Write8(results_size);
- for (size_t i = 0; i < results_size; ++i) {
- enc.WriteType(def.results[i].type);
- enc.WriteString(def.results[i].name);
- }
-
- return std::string{enc.ToStringView()};
-}
-
-bool UnpackRpcDefinition(std::string_view packed, RpcDefinition* def) {
- wpi::raw_mem_istream is(packed.data(), packed.size());
- wpi::Logger logger;
- WireDecoder dec(is, 0x0300, logger);
- if (!dec.Read8(&def->version)) {
- return false;
- }
- if (!dec.ReadString(&def->name)) {
- return false;
- }
-
- // parameters
- unsigned int params_size;
- if (!dec.Read8(¶ms_size)) {
- return false;
- }
- def->params.resize(0);
- def->params.reserve(params_size);
- for (size_t i = 0; i < params_size; ++i) {
- RpcParamDef pdef;
- NT_Type type;
- if (!dec.ReadType(&type)) {
- return false;
- }
- if (!dec.ReadString(&pdef.name)) {
- return false;
- }
- pdef.def_value = dec.ReadValue(type);
- if (!pdef.def_value) {
- return false;
- }
- def->params.emplace_back(std::move(pdef));
- }
-
- // results
- unsigned int results_size;
- if (!dec.Read8(&results_size)) {
- return false;
- }
- def->results.resize(0);
- def->results.reserve(results_size);
- for (size_t i = 0; i < results_size; ++i) {
- RpcResultDef rdef;
- if (!dec.ReadType(&rdef.type)) {
- return false;
- }
- if (!dec.ReadString(&rdef.name)) {
- return false;
- }
- def->results.emplace_back(std::move(rdef));
- }
-
- return true;
-}
-
-std::string PackRpcValues(wpi::span<const std::shared_ptr<Value>> values) {
- WireEncoder enc(0x0300);
- for (auto& value : values) {
- enc.WriteValue(*value);
- }
- return std::string{enc.ToStringView()};
-}
-
-std::vector<std::shared_ptr<Value>> UnpackRpcValues(
- std::string_view packed, wpi::span<const NT_Type> types) {
- wpi::raw_mem_istream is(packed.data(), packed.size());
- wpi::Logger logger;
- WireDecoder dec(is, 0x0300, logger);
- std::vector<std::shared_ptr<Value>> vec;
- for (auto type : types) {
- auto item = dec.ReadValue(type);
- if (!item) {
- return std::vector<std::shared_ptr<Value>>();
- }
- vec.emplace_back(std::move(item));
- }
- return vec;
-}
-
-uint64_t Now() {
- return wpi::Now();
}
/*
* Client/Server Functions
*/
-void SetNetworkIdentity(NT_Inst inst, std::string_view name) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
- }
-
- ii->dispatcher.SetIdentity(name);
-}
-
unsigned int GetNetworkMode(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return 0;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->networkMode;
+ } else {
+ return {};
}
-
- return ii->dispatcher.GetNetworkMode();
}
void StartLocal(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->StartLocal();
}
-
- ii->dispatcher.StartLocal();
}
void StopLocal(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->StopLocal();
}
-
- ii->dispatcher.Stop();
}
void StartServer(NT_Inst inst, std::string_view persist_filename,
- const char* listen_address, unsigned int port) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ const char* listen_address, unsigned int port3,
+ unsigned int port4) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->StartServer(persist_filename, listen_address, port3, port4);
}
-
- ii->dispatcher.StartServer(persist_filename, listen_address, port);
}
void StopServer(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->StopServer();
}
-
- ii->dispatcher.Stop();
}
-void StartClient(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+void StartClient3(NT_Inst inst, std::string_view identity) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->StartClient3(identity);
}
-
- ii->dispatcher.StartClient();
}
-void StartClient(NT_Inst inst, const char* server_name, unsigned int port) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+void StartClient4(NT_Inst inst, std::string_view identity) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->StartClient4(identity);
}
-
- ii->dispatcher.SetServer(server_name, port);
- ii->dispatcher.StartClient();
-}
-
-void StartClient(
- NT_Inst inst,
- wpi::span<const std::pair<std::string_view, unsigned int>> servers) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
- }
-
- ii->dispatcher.SetServer(servers);
- ii->dispatcher.StartClient();
-}
-
-void StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
- }
-
- ii->dispatcher.SetServerTeam(team, port);
- ii->dispatcher.StartClient();
}
void StopClient(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ ii->StopClient();
}
-
- ii->dispatcher.Stop();
}
void SetServer(NT_Inst inst, const char* server_name, unsigned int port) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
- }
-
- ii->dispatcher.SetServer(server_name, port);
+ SetServer(inst, {{{server_name, port}}});
}
void SetServer(
NT_Inst inst,
- wpi::span<const std::pair<std::string_view, unsigned int>> servers) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ std::span<const std::pair<std::string_view, unsigned int>> servers) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ std::vector<std::pair<std::string, unsigned int>> serversCopy;
+ serversCopy.reserve(servers.size());
+ for (auto&& server : servers) {
+ serversCopy.emplace_back(std::string{server.first}, server.second);
+ }
+ ii->SetServers(serversCopy);
}
-
- ii->dispatcher.SetServer(servers);
}
void SetServerTeam(NT_Inst inst, unsigned int team, unsigned int port) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
- }
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ std::vector<std::pair<std::string, unsigned int>> servers;
+ servers.reserve(5);
- ii->dispatcher.SetServerTeam(team, port);
+ // 10.te.am.2
+ servers.emplace_back(fmt::format("10.{}.{}.2", static_cast<int>(team / 100),
+ static_cast<int>(team % 100)),
+ port);
+
+ // 172.22.11.2
+ servers.emplace_back("172.22.11.2", port);
+
+ // roboRIO-<team>-FRC.local
+ servers.emplace_back(fmt::format("roboRIO-{}-FRC.local", team), port);
+
+ // roboRIO-<team>-FRC.lan
+ servers.emplace_back(fmt::format("roboRIO-{}-FRC.lan", team), port);
+
+ // roboRIO-<team>-FRC.frc-field.local
+ servers.emplace_back(fmt::format("roboRIO-{}-FRC.frc-field.local", team),
+ port);
+
+ ii->SetServers(servers);
+ }
}
void StartDSClient(NT_Inst inst, unsigned int port) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ if (auto client = ii->GetClient()) {
+ client->StartDSClient(port);
+ }
}
-
- ii->ds_client.Start(port);
}
void StopDSClient(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ if (auto client = ii->GetClient()) {
+ client->StopDSClient();
+ }
}
-
- ii->ds_client.Stop();
}
-void SetUpdateRate(NT_Inst inst, double interval) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+void FlushLocal(NT_Inst inst) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ if (auto client = ii->GetClient()) {
+ client->FlushLocal();
+ } else if (auto server = ii->GetServer()) {
+ server->FlushLocal();
+ }
}
-
- ii->dispatcher.SetUpdateRate(interval);
}
void Flush(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return;
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ if (auto client = ii->GetClient()) {
+ client->Flush();
+ } else if (auto server = ii->GetServer()) {
+ server->Flush();
+ }
}
-
- ii->dispatcher.Flush();
}
std::vector<ConnectionInfo> GetConnections(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->connectionList.GetConnections();
+ } else {
return {};
}
-
- return ii->dispatcher.GetConnections();
}
bool IsConnected(NT_Inst inst) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->networkMode == NT_NET_MODE_LOCAL ||
+ ii->connectionList.IsConnected();
+ } else {
return false;
}
-
- return ii->dispatcher.IsConnected();
}
-/*
- * Persistent Functions
- */
-
-const char* SavePersistent(NT_Inst inst, std::string_view filename) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return "invalid instance handle";
- }
-
- return ii->storage.SavePersistent(filename, false);
-}
-
-const char* LoadPersistent(
- NT_Inst inst, std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return "invalid instance handle";
- }
-
- return ii->storage.LoadPersistent(filename, warn);
-}
-
-const char* SaveEntries(NT_Inst inst, std::string_view filename,
- std::string_view prefix) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return "invalid instance handle";
- }
-
- return ii->storage.SaveEntries(filename, prefix);
-}
-
-const char* LoadEntries(
- NT_Inst inst, std::string_view filename, std::string_view prefix,
- std::function<void(size_t line, const char* msg)> warn) {
- auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance));
- if (!ii) {
- return "invalid instance handle";
- }
-
- return ii->storage.LoadEntries(filename, prefix, warn);
-}
-
-NT_Logger AddLogger(NT_Inst inst,
- std::function<void(const LogMessage& msg)> func,
- unsigned int minLevel, unsigned int maxLevel) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return 0;
- }
-
- if (minLevel < ii->logger.min_level()) {
- ii->logger.set_min_level(minLevel);
- }
-
- return Handle(i, ii->logger_impl.Add(func, minLevel, maxLevel),
- Handle::kLogger);
-}
-
-NT_LoggerPoller CreateLoggerPoller(NT_Inst inst) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return 0;
- }
-
- return Handle(i, ii->logger_impl.CreatePoller(), Handle::kLoggerPoller);
-}
-
-void DestroyLoggerPoller(NT_LoggerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kLoggerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
- }
-
- ii->logger_impl.RemovePoller(id);
-}
-
-NT_Logger AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
- unsigned int max_level) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kLoggerPoller);
- int i = handle.GetInst();
- auto ii = InstanceImpl::Get(i);
- if (id < 0 || !ii) {
- return 0;
- }
-
- if (min_level < ii->logger.min_level()) {
- ii->logger.set_min_level(min_level);
- }
-
- return Handle(i, ii->logger_impl.AddPolled(id, min_level, max_level),
- Handle::kLogger);
-}
-
-std::vector<LogMessage> PollLogger(NT_LoggerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kLoggerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
+std::optional<int64_t> GetServerTimeOffset(NT_Inst inst) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ return ii->GetServerTimeOffset();
+ } else {
return {};
}
-
- return ii->logger_impl.Poll(static_cast<unsigned int>(id));
}
-std::vector<LogMessage> PollLogger(NT_LoggerPoller poller, double timeout,
- bool* timed_out) {
- *timed_out = false;
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kLoggerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
+NT_Listener AddLogger(NT_Inst inst, unsigned int minLevel,
+ unsigned int maxLevel, ListenerCallback func) {
+ if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) {
+ if (minLevel < ii->logger.min_level()) {
+ ii->logger.set_min_level(minLevel);
+ }
+ auto listener = ii->listenerStorage.AddListener(std::move(func));
+ ii->logger_impl.AddListener(listener, minLevel, maxLevel);
+ return listener;
+ } else {
return {};
}
-
- return ii->logger_impl.Poll(static_cast<unsigned int>(id), timeout,
- timed_out);
}
-void CancelPollLogger(NT_LoggerPoller poller) {
- Handle handle{poller};
- int id = handle.GetTypedIndex(Handle::kLoggerPoller);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (id < 0 || !ii) {
- return;
+NT_Listener AddPolledLogger(NT_ListenerPoller poller, unsigned int minLevel,
+ unsigned int maxLevel) {
+ if (auto ii = InstanceImpl::GetTyped(poller, Handle::kListenerPoller)) {
+ if (minLevel < ii->logger.min_level()) {
+ ii->logger.set_min_level(minLevel);
+ }
+ auto listener = ii->listenerStorage.AddListener(poller);
+ ii->logger_impl.AddListener(listener, minLevel, maxLevel);
+ return listener;
+ } else {
+ return {};
}
-
- ii->logger_impl.CancelPoll(id);
-}
-
-void RemoveLogger(NT_Logger logger) {
- Handle handle{logger};
- int uid = handle.GetTypedIndex(Handle::kLogger);
- auto ii = InstanceImpl::Get(handle.GetInst());
- if (uid < 0 || !ii) {
- return;
- }
-
- ii->logger_impl.Remove(uid);
- ii->logger.set_min_level(ii->logger_impl.GetMinLevel());
-}
-
-bool WaitForLoggerQueue(NT_Inst inst, double timeout) {
- int i = Handle{inst}.GetTypedInst(Handle::kInstance);
- auto ii = InstanceImpl::Get(i);
- if (!ii) {
- return true;
- }
- return ii->logger_impl.WaitForQueue(timeout);
}
} // namespace nt
diff --git a/ntcore/src/main/native/cpp/ntcore_meta.cpp b/ntcore/src/main/native/cpp/ntcore_meta.cpp
new file mode 100644
index 0000000..05cb4b7
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ntcore_meta.cpp
@@ -0,0 +1,210 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <fmt/format.h>
+#include <wpi/MessagePack.h>
+#include <wpi/mpack.h>
+
+#include "ntcore_cpp.h"
+
+using namespace mpack;
+
+using namespace nt::meta;
+
+static SubscriberOptions DecodeSubscriberOptions(mpack_reader_t& r) {
+ SubscriberOptions options;
+ uint32_t numMapElem = mpack_expect_map(&r);
+ for (uint32_t j = 0; j < numMapElem; ++j) {
+ std::string key;
+ mpack_expect_str(&r, &key);
+ if (key == "topicsonly") {
+ options.topicsOnly = mpack_expect_bool(&r);
+ } else if (key == "all") {
+ options.sendAll = mpack_expect_bool(&r);
+ } else if (key == "periodic") {
+ options.periodic = mpack_expect_float(&r);
+ } else if (key == "prefix") {
+ options.prefixMatch = mpack_expect_bool(&r);
+ } else {
+ // TODO: Save other options
+ mpack_discard(&r);
+ }
+ }
+ mpack_done_map(&r);
+ return options;
+}
+
+std::optional<std::vector<ClientPublisher>> nt::meta::DecodeClientPublishers(
+ std::span<const uint8_t> data) {
+ mpack_reader_t r;
+ mpack_reader_init_data(&r, data);
+ uint32_t numPub = mpack_expect_array_max(&r, 1000);
+ std::vector<ClientPublisher> publishers;
+ publishers.reserve(numPub);
+ for (uint32_t i = 0; i < numPub; ++i) {
+ ClientPublisher pub;
+ uint32_t numMapElem = mpack_expect_map(&r);
+ for (uint32_t j = 0; j < numMapElem; ++j) {
+ std::string key;
+ mpack_expect_str(&r, &key);
+ if (key == "uid") {
+ pub.uid = mpack_expect_i64(&r);
+ } else if (key == "topic") {
+ mpack_expect_str(&r, &pub.topic);
+ } else {
+ mpack_discard(&r);
+ }
+ }
+ mpack_done_map(&r);
+ publishers.emplace_back(std::move(pub));
+ }
+ mpack_done_array(&r);
+ if (mpack_reader_destroy(&r) == mpack_ok) {
+ return {std::move(publishers)};
+ } else {
+ return {};
+ }
+}
+
+std::optional<std::vector<ClientSubscriber>> nt::meta::DecodeClientSubscribers(
+ std::span<const uint8_t> data) {
+ mpack_reader_t r;
+ mpack_reader_init_data(&r, data);
+ uint32_t numSub = mpack_expect_array_max(&r, 1000);
+ std::vector<ClientSubscriber> subscribers;
+ subscribers.reserve(numSub);
+ for (uint32_t i = 0; i < numSub; ++i) {
+ ClientSubscriber sub;
+ uint32_t numMapElem = mpack_expect_map(&r);
+ for (uint32_t j = 0; j < numMapElem; ++j) {
+ std::string key;
+ mpack_expect_str(&r, &key);
+ if (key == "uid") {
+ sub.uid = mpack_expect_i64(&r);
+ } else if (key == "topics") {
+ uint32_t numPrefix = mpack_expect_array_max(&r, 100);
+ sub.topics.reserve(numPrefix);
+ for (uint32_t k = 0; k < numPrefix; ++k) {
+ std::string val;
+ if (mpack_expect_str(&r, &val) == mpack_ok) {
+ sub.topics.emplace_back(std::move(val));
+ }
+ }
+ mpack_done_array(&r);
+ } else if (key == "options") {
+ sub.options = DecodeSubscriberOptions(r);
+ } else {
+ mpack_discard(&r);
+ }
+ }
+ mpack_done_map(&r);
+ subscribers.emplace_back(std::move(sub));
+ }
+ mpack_done_array(&r);
+ if (mpack_reader_destroy(&r) == mpack_ok) {
+ return {std::move(subscribers)};
+ } else {
+ return {};
+ }
+}
+
+std::optional<std::vector<TopicPublisher>> nt::meta::DecodeTopicPublishers(
+ std::span<const uint8_t> data) {
+ mpack_reader_t r;
+ mpack_reader_init_data(&r, data);
+ uint32_t numPub = mpack_expect_array_max(&r, 1000);
+ std::vector<TopicPublisher> publishers;
+ publishers.reserve(numPub);
+ for (uint32_t i = 0; i < numPub; ++i) {
+ TopicPublisher pub;
+ uint32_t numMapElem = mpack_expect_map(&r);
+ for (uint32_t j = 0; j < numMapElem; ++j) {
+ std::string key;
+ mpack_expect_str(&r, &key);
+ if (key == "pubuid") {
+ pub.pubuid = mpack_expect_i64(&r);
+ } else if (key == "client") {
+ mpack_expect_str(&r, &pub.client);
+ } else {
+ mpack_discard(&r);
+ }
+ }
+ mpack_done_map(&r);
+ publishers.emplace_back(std::move(pub));
+ }
+ mpack_done_array(&r);
+ if (mpack_reader_destroy(&r) == mpack_ok) {
+ return {std::move(publishers)};
+ } else {
+ return {};
+ }
+}
+
+std::optional<std::vector<TopicSubscriber>> nt::meta::DecodeTopicSubscribers(
+ std::span<const uint8_t> data) {
+ mpack_reader_t r;
+ mpack_reader_init_data(&r, data);
+ uint32_t numSub = mpack_expect_array_max(&r, 1000);
+ std::vector<TopicSubscriber> subscribers;
+ subscribers.reserve(numSub);
+ for (uint32_t i = 0; i < numSub; ++i) {
+ TopicSubscriber sub;
+ uint32_t numMapElem = mpack_expect_map(&r);
+ for (uint32_t j = 0; j < numMapElem; ++j) {
+ std::string key;
+ mpack_expect_str(&r, &key);
+ if (key == "subuid") {
+ sub.subuid = mpack_expect_i64(&r);
+ } else if (key == "client") {
+ mpack_expect_str(&r, &sub.client);
+ } else if (key == "options") {
+ sub.options = DecodeSubscriberOptions(r);
+ } else {
+ mpack_discard(&r);
+ }
+ }
+ mpack_done_map(&r);
+ subscribers.emplace_back(std::move(sub));
+ }
+ mpack_done_array(&r);
+ if (mpack_reader_destroy(&r) == mpack_ok) {
+ return {std::move(subscribers)};
+ } else {
+ return {};
+ }
+}
+
+std::optional<std::vector<Client>> nt::meta::DecodeClients(
+ std::span<const uint8_t> data) {
+ mpack_reader_t r;
+ mpack_reader_init_data(&r, data);
+ uint32_t numClients = mpack_expect_array_max(&r, 100);
+ std::vector<Client> clients;
+ clients.reserve(numClients);
+ for (uint32_t i = 0; i < numClients; ++i) {
+ Client client;
+ uint32_t numMapElem = mpack_expect_map(&r);
+ for (uint32_t j = 0; j < numMapElem; ++j) {
+ std::string key;
+ mpack_expect_str(&r, &key);
+ if (key == "id") {
+ mpack_expect_str(&r, &client.id);
+ } else if (key == "conn") {
+ mpack_expect_str(&r, &client.conn);
+ } else if (key == "ver") {
+ client.version = mpack_expect_u16(&r);
+ } else {
+ mpack_discard(&r);
+ }
+ }
+ mpack_done_map(&r);
+ clients.emplace_back(std::move(client));
+ }
+ mpack_done_array(&r);
+ if (mpack_reader_destroy(&r) == mpack_ok) {
+ return {std::move(clients)};
+ } else {
+ return {};
+ }
+}
diff --git a/ntcore/src/main/native/cpp/ntcore_meta_c.cpp b/ntcore/src/main/native/cpp/ntcore_meta_c.cpp
new file mode 100644
index 0000000..889254e
--- /dev/null
+++ b/ntcore/src/main/native/cpp/ntcore_meta_c.cpp
@@ -0,0 +1,138 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <optional>
+#include <vector>
+
+#include "Value_internal.h"
+#include "ntcore_cpp.h"
+
+using namespace nt;
+using namespace nt::meta;
+
+static void ConvertToC(const SubscriberOptions& in,
+ NT_Meta_SubscriberOptions* out) {
+ out->periodic = in.periodic;
+ out->topicsOnly = in.topicsOnly;
+ out->sendAll = in.sendAll;
+ out->prefixMatch = in.prefixMatch;
+}
+
+static void ConvertToC(const TopicPublisher& in, NT_Meta_TopicPublisher* out) {
+ ConvertToC(in.client, &out->client);
+ out->pubuid = in.pubuid;
+}
+
+static void ConvertToC(const TopicSubscriber& in,
+ NT_Meta_TopicSubscriber* out) {
+ ConvertToC(in.client, &out->client);
+ out->subuid = in.subuid;
+ ConvertToC(in.options, &out->options);
+}
+
+static void ConvertToC(const ClientPublisher& in,
+ NT_Meta_ClientPublisher* out) {
+ out->uid = in.uid;
+ ConvertToC(in.topic, &out->topic);
+}
+
+static void ConvertToC(const ClientSubscriber& in,
+ NT_Meta_ClientSubscriber* out) {
+ out->uid = in.uid;
+ out->topics = ConvertToC<NT_String>(in.topics, &out->topicsCount);
+ ConvertToC(in.options, &out->options);
+}
+
+static void ConvertToC(const Client& in, NT_Meta_Client* out) {
+ ConvertToC(in.id, &out->id);
+ ConvertToC(in.conn, &out->conn);
+ out->version = in.version;
+}
+
+template <typename O, typename I>
+static O* ConvertToC(const std::optional<std::vector<I>>& in, size_t* out_len) {
+ if (in) {
+ if (O* rv = ConvertToC<O>(*in, out_len)) {
+ return rv;
+ } else {
+ return static_cast<O*>(wpi::safe_malloc(0)); // return non-NULL
+ }
+ } else {
+ *out_len = 0;
+ return nullptr;
+ }
+}
+
+extern "C" {
+
+struct NT_Meta_TopicPublisher* NT_Meta_DecodeTopicPublishers(
+ const uint8_t* data, size_t size, size_t* count) {
+ return ConvertToC<NT_Meta_TopicPublisher>(DecodeTopicPublishers({data, size}),
+ count);
+}
+
+struct NT_Meta_TopicSubscriber* NT_Meta_DecodeTopicSubscribers(
+ const uint8_t* data, size_t size, size_t* count) {
+ return ConvertToC<NT_Meta_TopicSubscriber>(
+ DecodeTopicSubscribers({data, size}), count);
+}
+
+struct NT_Meta_ClientPublisher* NT_Meta_DecodeClientPublishers(
+ const uint8_t* data, size_t size, size_t* count) {
+ return ConvertToC<NT_Meta_ClientPublisher>(
+ DecodeClientPublishers({data, size}), count);
+}
+
+struct NT_Meta_ClientSubscriber* NT_Meta_DecodeClientSubscribers(
+ const uint8_t* data, size_t size, size_t* count) {
+ return ConvertToC<NT_Meta_ClientSubscriber>(
+ DecodeClientSubscribers({data, size}), count);
+}
+
+struct NT_Meta_Client* NT_Meta_DecodeClients(const uint8_t* data, size_t size,
+ size_t* count) {
+ return ConvertToC<NT_Meta_Client>(DecodeClients({data, size}), count);
+}
+
+void NT_Meta_FreeTopicPublishers(struct NT_Meta_TopicPublisher* arr,
+ size_t count) {
+ for (size_t i = 0; i < count; ++i) {
+ std::free(arr[i].client.str);
+ }
+ std::free(arr);
+}
+
+void NT_Meta_FreeTopicSubscribers(struct NT_Meta_TopicSubscriber* arr,
+ size_t count) {
+ for (size_t i = 0; i < count; ++i) {
+ std::free(arr[i].client.str);
+ }
+ std::free(arr);
+}
+
+void NT_Meta_FreeClientPublishers(struct NT_Meta_ClientPublisher* arr,
+ size_t count) {
+ for (size_t i = 0; i < count; ++i) {
+ std::free(arr[i].topic.str);
+ }
+ std::free(arr);
+}
+
+void NT_Meta_FreeClientSubscribers(struct NT_Meta_ClientSubscriber* arr,
+ size_t count) {
+ for (size_t i = 0; i < count; ++i) {
+ NT_FreeStringArray(arr[i].topics, arr[i].topicsCount);
+ }
+ std::free(arr);
+}
+
+void NT_Meta_FreeClients(struct NT_Meta_Client* arr, size_t count) {
+ for (size_t i = 0; i < count; ++i) {
+ std::free(arr[i].id.str);
+ std::free(arr[i].conn.str);
+ }
+ std::free(arr);
+}
+
+} // extern "C"
diff --git a/ntcore/src/main/native/cpp/ntcore_test.cpp b/ntcore/src/main/native/cpp/ntcore_test.cpp
index 64bde68..2a07867 100644
--- a/ntcore/src/main/native/cpp/ntcore_test.cpp
+++ b/ntcore/src/main/native/cpp/ntcore_test.cpp
@@ -11,31 +11,30 @@
#include "Value_internal.h"
extern "C" {
-struct NT_String* NT_GetStringForTesting(const char* string, int* struct_size) {
- struct NT_String* str =
+struct NT_String* NT_GetStringForTesting(const char* str, int* struct_size) {
+ struct NT_String* strout =
static_cast<NT_String*>(wpi::safe_calloc(1, sizeof(NT_String)));
- nt::ConvertToC(string, str);
+ nt::ConvertToC(str, strout);
*struct_size = sizeof(NT_String);
- return str;
+ return strout;
}
-struct NT_EntryInfo* NT_GetEntryInfoForTesting(const char* name,
+struct NT_TopicInfo* NT_GetTopicInfoForTesting(const char* name,
enum NT_Type type,
- unsigned int flags,
- uint64_t last_change,
+ const char* type_str,
int* struct_size) {
- struct NT_EntryInfo* entry_info =
- static_cast<NT_EntryInfo*>(wpi::safe_calloc(1, sizeof(NT_EntryInfo)));
- nt::ConvertToC(name, &entry_info->name);
- entry_info->type = type;
- entry_info->flags = flags;
- entry_info->last_change = last_change;
- *struct_size = sizeof(NT_EntryInfo);
- return entry_info;
+ struct NT_TopicInfo* topic_info =
+ static_cast<NT_TopicInfo*>(wpi::safe_calloc(1, sizeof(NT_TopicInfo)));
+ nt::ConvertToC(name, &topic_info->name);
+ topic_info->type = type;
+ nt::ConvertToC(type_str, &topic_info->type_str);
+ *struct_size = sizeof(NT_TopicInfo);
+ return topic_info;
}
-void NT_FreeEntryInfoForTesting(struct NT_EntryInfo* info) {
+void NT_FreeTopicInfoForTesting(struct NT_TopicInfo* info) {
std::free(info->name.str);
+ std::free(info->type_str.str);
std::free(info);
}
@@ -158,88 +157,4 @@
}
// No need for free as one already exists in the main library
-static void CopyNtValue(const struct NT_Value* copy_from,
- struct NT_Value* copy_to) {
- auto cpp_value = nt::ConvertFromC(*copy_from);
- nt::ConvertToC(*cpp_value, copy_to);
-}
-
-static void CopyNtString(const struct NT_String* copy_from,
- struct NT_String* copy_to) {
- nt::ConvertToC({copy_from->str, copy_from->len}, copy_to);
-}
-
-struct NT_RpcParamDef* NT_GetRpcParamDefForTesting(const char* name,
- const struct NT_Value* val,
- int* struct_size) {
- struct NT_RpcParamDef* def =
- static_cast<NT_RpcParamDef*>(wpi::safe_calloc(1, sizeof(NT_RpcParamDef)));
- nt::ConvertToC(name, &def->name);
- CopyNtValue(val, &def->def_value);
- *struct_size = sizeof(NT_RpcParamDef);
- return def;
-}
-
-void NT_FreeRpcParamDefForTesting(struct NT_RpcParamDef* def) {
- NT_DisposeValue(&def->def_value);
- NT_DisposeString(&def->name);
- std::free(def);
-}
-
-struct NT_RpcResultDef* NT_GetRpcResultsDefForTesting(const char* name,
- enum NT_Type type,
- int* struct_size) {
- struct NT_RpcResultDef* def = static_cast<NT_RpcResultDef*>(
- wpi::safe_calloc(1, sizeof(NT_RpcResultDef)));
- nt::ConvertToC(name, &def->name);
- def->type = type;
- *struct_size = sizeof(NT_RpcResultDef);
- return def;
-}
-
-void NT_FreeRpcResultsDefForTesting(struct NT_RpcResultDef* def) {
- NT_DisposeString(&def->name);
- std::free(def);
-}
-
-struct NT_RpcDefinition* NT_GetRpcDefinitionForTesting(
- unsigned int version, const char* name, size_t num_params,
- const struct NT_RpcParamDef* params, size_t num_results,
- const struct NT_RpcResultDef* results, int* struct_size) {
- struct NT_RpcDefinition* def = static_cast<NT_RpcDefinition*>(
- wpi::safe_calloc(1, sizeof(NT_RpcDefinition)));
- def->version = version;
- nt::ConvertToC(name, &def->name);
- def->num_params = num_params;
- def->params = static_cast<NT_RpcParamDef*>(
- wpi::safe_malloc(num_params * sizeof(NT_RpcParamDef)));
- for (size_t i = 0; i < num_params; ++i) {
- CopyNtString(¶ms[i].name, &def->params[i].name);
- CopyNtValue(¶ms[i].def_value, &def->params[i].def_value);
- }
- def->num_results = num_results;
- def->results = static_cast<NT_RpcResultDef*>(
- wpi::safe_malloc(num_results * sizeof(NT_RpcResultDef)));
- for (size_t i = 0; i < num_results; ++i) {
- CopyNtString(&results[i].name, &def->results[i].name);
- def->results[i].type = results[i].type;
- }
- *struct_size = sizeof(NT_RpcDefinition);
- return def;
-}
-// No need for free as one already exists in the main library
-
-struct NT_RpcAnswer* NT_GetRpcAnswerForTesting(
- unsigned int rpc_id, unsigned int call_uid, const char* name,
- const char* params, size_t params_len, int* struct_size) {
- struct NT_RpcAnswer* info =
- static_cast<NT_RpcAnswer*>(wpi::safe_calloc(1, sizeof(NT_RpcAnswer)));
- info->entry = rpc_id;
- info->call = call_uid;
- nt::ConvertToC(name, &info->name);
- nt::ConvertToC({params, params_len}, &info->params);
- *struct_size = sizeof(NT_RpcAnswer);
- return info;
-}
-// No need for free as one already exists in the main library
} // extern "C"
diff --git a/ntcore/src/main/native/include/networktables/EntryListenerFlags.h b/ntcore/src/main/native/include/networktables/EntryListenerFlags.h
deleted file mode 100644
index bbf5e42..0000000
--- a/ntcore/src/main/native/include/networktables/EntryListenerFlags.h
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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.
-
-#ifndef NTCORE_NETWORKTABLES_ENTRYLISTENERFLAGS_H_
-#define NTCORE_NETWORKTABLES_ENTRYLISTENERFLAGS_H_
-
-#include "ntcore_c.h"
-
-/** Entry listener flags */
-namespace nt::EntryListenerFlags {
-
-/**
- * Flag values for use with entry listeners.
- *
- * The flags are a bitmask and must be OR'ed together to indicate the
- * combination of events desired to be received.
- *
- * The constants kNew, kDelete, kUpdate, and kFlags represent different events
- * that can occur to entries.
- *
- * By default, notifications are only generated for remote changes occurring
- * after the listener is created. The constants kImmediate and kLocal are
- * modifiers that cause notifications to be generated at other times.
- *
- * @ingroup ntcore_cpp_api
- */
-enum {
- /**
- * Initial listener addition.
- * Set this flag to receive immediate notification of entries matching the
- * flag criteria (generally only useful when combined with kNew).
- */
- kImmediate = NT_NOTIFY_IMMEDIATE,
-
- /**
- * Changed locally.
- * Set this flag to receive notification of both local changes and changes
- * coming from remote nodes. By default, notifications are only generated
- * for remote changes. Must be combined with some combination of kNew,
- * kDelete, kUpdate, and kFlags to receive notifications of those respective
- * events.
- */
- kLocal = NT_NOTIFY_LOCAL,
-
- /**
- * Newly created entry.
- * Set this flag to receive a notification when an entry is created.
- */
- kNew = NT_NOTIFY_NEW,
-
- /**
- * Entry was deleted.
- * Set this flag to receive a notification when an entry is deleted.
- */
- kDelete = NT_NOTIFY_DELETE,
-
- /**
- * Entry's value changed.
- * Set this flag to receive a notification when an entry's value (or type)
- * changes.
- */
- kUpdate = NT_NOTIFY_UPDATE,
-
- /**
- * Entry's flags changed.
- * Set this flag to receive a notification when an entry's flags value
- * changes.
- */
- kFlags = NT_NOTIFY_FLAGS
-};
-
-} // namespace nt::EntryListenerFlags
-
-#endif // NTCORE_NETWORKTABLES_ENTRYLISTENERFLAGS_H_
diff --git a/ntcore/src/main/native/include/networktables/GenericEntry.h b/ntcore/src/main/native/include/networktables/GenericEntry.h
new file mode 100644
index 0000000..6c7fee4
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/GenericEntry.h
@@ -0,0 +1,483 @@
+// 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 <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "networktables/Topic.h"
+
+namespace nt {
+
+class Value;
+
+/**
+ * NetworkTables generic subscriber.
+ */
+class GenericSubscriber : public Subscriber {
+ public:
+ using TopicType = Topic;
+ using ValueType = Value;
+ using ParamType = const Value&;
+ using TimestampedValueType = Value;
+
+ GenericSubscriber() = default;
+
+ /**
+ * Construct from a subscriber handle; recommended to use
+ * Topic::GenericSubscribe() instead.
+ *
+ * @param handle Native handle
+ */
+ explicit GenericSubscriber(NT_Subscriber handle);
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns a value with unassigned type.
+ *
+ * @return value
+ */
+ ValueType Get() const;
+
+ /**
+ * Gets the entry's value as a boolean. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ bool GetBoolean(bool defaultValue) const;
+
+ /**
+ * Gets the entry's value as a integer. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ int64_t GetInteger(int64_t defaultValue) const;
+
+ /**
+ * Gets the entry's value as a float. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ float GetFloat(float defaultValue) const;
+
+ /**
+ * Gets the entry's value as a double. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ double GetDouble(double defaultValue) const;
+
+ /**
+ * Gets the entry's value as a string. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ std::string GetString(std::string_view defaultValue) const;
+
+ /**
+ * Gets the entry's value as a raw. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ std::vector<uint8_t> GetRaw(std::span<const uint8_t> defaultValue) const;
+
+ /**
+ * Gets the entry's value as a boolean array. If the entry does not exist
+ * or is of different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ *
+ * @note This makes a copy of the array. If the overhead of this is a
+ * concern, use GetValue() instead.
+ *
+ * @note The returned array is std::vector<int> instead of std::vector<bool>
+ * because std::vector<bool> is special-cased in C++. 0 is false, any
+ * non-zero value is true.
+ */
+ std::vector<int> GetBooleanArray(std::span<const int> defaultValue) const;
+
+ /**
+ * Gets the entry's value as a integer array. If the entry does not exist
+ * or is of different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ *
+ * @note This makes a copy of the array. If the overhead of this is a
+ * concern, use GetValue() instead.
+ */
+ std::vector<int64_t> GetIntegerArray(
+ std::span<const int64_t> defaultValue) const;
+
+ /**
+ * Gets the entry's value as a float array. If the entry does not exist
+ * or is of different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ *
+ * @note This makes a copy of the array. If the overhead of this is a
+ * concern, use GetValue() instead.
+ */
+ std::vector<float> GetFloatArray(std::span<const float> defaultValue) const;
+
+ /**
+ * Gets the entry's value as a double array. If the entry does not exist
+ * or is of different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ *
+ * @note This makes a copy of the array. If the overhead of this is a
+ * concern, use GetValue() instead.
+ */
+ std::vector<double> GetDoubleArray(
+ std::span<const double> defaultValue) const;
+
+ /**
+ * Gets the entry's value as a string array. If the entry does not exist
+ * or is of different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ *
+ * @note This makes a copy of the array. If the overhead of this is a
+ * concern, use GetValue() instead.
+ */
+ std::vector<std::string> GetStringArray(
+ std::span<const std::string> defaultValue) const;
+
+ /**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @return Array of timestamped values; empty array if no new changes have
+ * been published since the previous call.
+ */
+ std::vector<TimestampedValueType> ReadQueue();
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+};
+
+/**
+ * NetworkTables generic publisher.
+ */
+class GenericPublisher : public Publisher {
+ public:
+ using TopicType = Topic;
+ using ValueType = Value;
+ using ParamType = const Value&;
+ using TimestampedValueType = Value;
+
+ GenericPublisher() = default;
+
+ /**
+ * Construct from a publisher handle; recommended to use
+ * Topic::GenericPublish() instead.
+ *
+ * @param handle Native handle
+ */
+ explicit GenericPublisher(NT_Publisher handle);
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ */
+ void Set(ParamType value);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetBoolean(bool value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetInteger(int64_t value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetFloat(float value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetDouble(double value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetString(std::string_view value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetRaw(std::span<const uint8_t> value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetBooleanArray(std::span<const bool> value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetBooleanArray(std::span<const int> value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetIntegerArray(std::span<const int64_t> value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetFloatArray(std::span<const float> value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetDoubleArray(std::span<const double> value, int64_t time = 0);
+
+ /**
+ * Sets the entry's value.
+ *
+ * @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
+ * @return False if the entry exists with a different type
+ */
+ bool SetStringArray(std::span<const std::string> value, int64_t time = 0);
+
+ /**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param value value
+ */
+ void SetDefault(ParamType value);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultBoolean(bool defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultInteger(int64_t defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultFloat(float defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultDouble(double defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultString(std::string_view defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultRaw(std::span<const uint8_t> defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultBooleanArray(std::span<const int> defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultIntegerArray(std::span<const int64_t> defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultFloatArray(std::span<const float> defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultDoubleArray(std::span<const double> defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultStringArray(std::span<const std::string> defaultValue);
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+};
+
+/**
+ * NetworkTables generic entry.
+ *
+ * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
+ */
+class GenericEntry final : public GenericSubscriber, public GenericPublisher {
+ public:
+ using SubscriberType = GenericSubscriber;
+ using PublisherType = GenericPublisher;
+ using TopicType = Topic;
+ using ValueType = Value;
+ using ParamType = const Value&;
+ using TimestampedValueType = Value;
+
+ GenericEntry() = default;
+
+ /**
+ * Construct from an entry handle; recommended to use
+ * RawTopic::GetEntry() instead.
+ *
+ * @param handle Native handle
+ */
+ explicit GenericEntry(NT_Entry handle);
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ explicit operator bool() const { return m_subHandle != 0; }
+
+ /**
+ * Gets the native handle for the entry.
+ *
+ * @return Native handle
+ */
+ NT_Entry GetHandle() const { return m_subHandle; }
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+
+ /**
+ * Stops publishing the entry if it's published.
+ */
+ void Unpublish();
+};
+
+} // namespace nt
+
+#include "networktables/GenericEntry.inc"
diff --git a/ntcore/src/main/native/include/networktables/GenericEntry.inc b/ntcore/src/main/native/include/networktables/GenericEntry.inc
new file mode 100644
index 0000000..f3d9967
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/GenericEntry.inc
@@ -0,0 +1,214 @@
+// 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 <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "networktables/GenericEntry.h"
+#include "networktables/NetworkTableType.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline GenericSubscriber::GenericSubscriber(NT_Subscriber handle)
+ : Subscriber{handle} {}
+
+inline Value GenericSubscriber::Get() const {
+ return ::nt::GetEntryValue(m_subHandle);
+}
+
+inline bool GenericSubscriber::GetBoolean(bool defaultValue) const {
+ return ::nt::GetBoolean(m_subHandle, defaultValue);
+}
+
+inline int64_t GenericSubscriber::GetInteger(int64_t defaultValue) const {
+ return ::nt::GetInteger(m_subHandle, defaultValue);
+}
+
+inline float GenericSubscriber::GetFloat(float defaultValue) const {
+ return ::nt::GetFloat(m_subHandle, defaultValue);
+}
+
+inline double GenericSubscriber::GetDouble(double defaultValue) const {
+ return ::nt::GetDouble(m_subHandle, defaultValue);
+}
+
+inline std::string GenericSubscriber::GetString(
+ std::string_view defaultValue) const {
+ return ::nt::GetString(m_subHandle, defaultValue);
+}
+
+inline std::vector<uint8_t> GenericSubscriber::GetRaw(
+ std::span<const uint8_t> defaultValue) const {
+ return ::nt::GetRaw(m_subHandle, defaultValue);
+}
+
+inline std::vector<int> GenericSubscriber::GetBooleanArray(
+ std::span<const int> defaultValue) const {
+ return ::nt::GetBooleanArray(m_subHandle, defaultValue);
+}
+
+inline std::vector<int64_t> GenericSubscriber::GetIntegerArray(
+ std::span<const int64_t> defaultValue) const {
+ return ::nt::GetIntegerArray(m_subHandle, defaultValue);
+}
+
+inline std::vector<float> GenericSubscriber::GetFloatArray(
+ std::span<const float> defaultValue) const {
+ return ::nt::GetFloatArray(m_subHandle, defaultValue);
+}
+
+inline std::vector<double> GenericSubscriber::GetDoubleArray(
+ std::span<const double> defaultValue) const {
+ return ::nt::GetDoubleArray(m_subHandle, defaultValue);
+}
+
+inline std::vector<std::string> GenericSubscriber::GetStringArray(
+ std::span<const std::string> defaultValue) const {
+ return ::nt::GetStringArray(m_subHandle, defaultValue);
+}
+
+inline std::vector<Value> GenericSubscriber::ReadQueue() {
+ return ::nt::ReadQueueValue(m_subHandle);
+}
+
+inline Topic GenericSubscriber::GetTopic() const {
+ return Topic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline GenericPublisher::GenericPublisher(NT_Publisher handle)
+ : Publisher{handle} {}
+
+inline void GenericPublisher::Set(const Value& value) {
+ ::nt::SetEntryValue(m_pubHandle, value);
+}
+
+inline bool GenericPublisher::SetBoolean(bool value, int64_t time) {
+ return nt::SetBoolean(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetInteger(int64_t value, int64_t time) {
+ return nt::SetInteger(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetFloat(float value, int64_t time) {
+ return nt::SetFloat(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetDouble(double value, int64_t time) {
+ return nt::SetDouble(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetString(std::string_view value, int64_t time) {
+ return nt::SetString(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetRaw(std::span<const uint8_t> value,
+ int64_t time) {
+ return nt::SetRaw(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetBooleanArray(std::span<const bool> value,
+ int64_t time) {
+ return SetEntryValue(m_pubHandle, Value::MakeBooleanArray(value, time));
+}
+
+inline bool GenericPublisher::SetBooleanArray(std::span<const int> value,
+ int64_t time) {
+ return nt::SetBooleanArray(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetIntegerArray(std::span<const int64_t> value,
+ int64_t time) {
+ return nt::SetIntegerArray(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetFloatArray(std::span<const float> value,
+ int64_t time) {
+ return nt::SetFloatArray(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetDoubleArray(std::span<const double> value,
+ int64_t time) {
+ return nt::SetDoubleArray(m_pubHandle, value, time);
+}
+
+inline bool GenericPublisher::SetStringArray(std::span<const std::string> value,
+ int64_t time) {
+ return nt::SetStringArray(m_pubHandle, value, time);
+}
+
+inline void GenericPublisher::SetDefault(const Value& value) {
+ ::nt::SetDefaultEntryValue(m_pubHandle, value);
+}
+
+inline bool GenericPublisher::SetDefaultBoolean(bool defaultValue) {
+ return nt::SetDefaultBoolean(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultInteger(int64_t defaultValue) {
+ return nt::SetDefaultInteger(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultFloat(float defaultValue) {
+ return nt::SetDefaultFloat(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultDouble(double defaultValue) {
+ return nt::SetDefaultDouble(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultString(std::string_view defaultValue) {
+ return nt::SetDefaultString(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultRaw(
+ std::span<const uint8_t> defaultValue) {
+ return nt::SetDefaultRaw(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultBooleanArray(
+ std::span<const int> defaultValue) {
+ return nt::SetDefaultBooleanArray(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultIntegerArray(
+ std::span<const int64_t> defaultValue) {
+ return nt::SetDefaultIntegerArray(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultFloatArray(
+ std::span<const float> defaultValue) {
+ return nt::SetDefaultFloatArray(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultDoubleArray(
+ std::span<const double> defaultValue) {
+ return nt::SetDefaultDoubleArray(m_pubHandle, defaultValue);
+}
+
+inline bool GenericPublisher::SetDefaultStringArray(
+ std::span<const std::string> defaultValue) {
+ return nt::SetDefaultStringArray(m_pubHandle, defaultValue);
+}
+
+inline Topic GenericPublisher::GetTopic() const {
+ return Topic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+inline GenericEntry::GenericEntry(NT_Entry handle)
+ : GenericSubscriber{handle}, GenericPublisher{handle} {}
+
+inline Topic GenericEntry::GetTopic() const {
+ return Topic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline void GenericEntry::Unpublish() {
+ ::nt::Unpublish(m_pubHandle);
+}
+} // namespace nt
diff --git a/ntcore/src/main/native/include/networktables/MultiSubscriber.h b/ntcore/src/main/native/include/networktables/MultiSubscriber.h
new file mode 100644
index 0000000..f146351
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/MultiSubscriber.h
@@ -0,0 +1,61 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <span>
+#include <string_view>
+
+#include "networktables/NetworkTableInstance.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+/**
+ * Subscribe to multiple topics based on one or more topic name prefixes. Can be
+ * used in combination with ValueListenerPoller to listen for value changes
+ * across all matching topics.
+ */
+class MultiSubscriber final {
+ public:
+ MultiSubscriber() = default;
+
+ /**
+ * Create a multiple subscriber.
+ *
+ * @param inst instance
+ * @param prefixes topic name prefixes
+ * @param options subscriber options
+ */
+ MultiSubscriber(NetworkTableInstance inst,
+ std::span<const std::string_view> prefixes,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ MultiSubscriber(const MultiSubscriber&) = delete;
+ MultiSubscriber& operator=(const MultiSubscriber&) = delete;
+ MultiSubscriber(MultiSubscriber&& rhs);
+ MultiSubscriber& operator=(MultiSubscriber&& rhs);
+ ~MultiSubscriber();
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ explicit operator bool() const { return m_handle != 0; }
+
+ /**
+ * Gets the native handle.
+ *
+ * @return Handle
+ */
+ NT_MultiSubscriber GetHandle() const { return m_handle; }
+
+ private:
+ NT_MultiSubscriber m_handle{0};
+};
+
+} // namespace nt
+
+#include "MultiSubscriber.inc"
diff --git a/ntcore/src/main/native/include/networktables/MultiSubscriber.inc b/ntcore/src/main/native/include/networktables/MultiSubscriber.inc
new file mode 100644
index 0000000..c32c06a
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/MultiSubscriber.inc
@@ -0,0 +1,36 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include "networktables/MultiSubscriber.h"
+
+namespace nt {
+
+inline MultiSubscriber::MultiSubscriber(
+ NetworkTableInstance inst, std::span<const std::string_view> prefixes,
+ const PubSubOptions& options)
+ : m_handle{::nt::SubscribeMultiple(inst.GetHandle(), prefixes, options)} {}
+
+inline MultiSubscriber::MultiSubscriber(MultiSubscriber&& rhs)
+ : m_handle{rhs.m_handle} {
+ rhs.m_handle = 0;
+}
+
+inline MultiSubscriber& MultiSubscriber::operator=(MultiSubscriber&& rhs) {
+ if (m_handle != 0) {
+ ::nt::UnsubscribeMultiple(m_handle);
+ }
+ m_handle = rhs.m_handle;
+ rhs.m_handle = 0;
+ return *this;
+}
+
+inline MultiSubscriber::~MultiSubscriber() {
+ if (m_handle != 0) {
+ ::nt::UnsubscribeMultiple(m_handle);
+ }
+}
+
+} // namespace nt
diff --git a/ntcore/src/main/native/include/networktables/NTSendableBuilder.h b/ntcore/src/main/native/include/networktables/NTSendableBuilder.h
index 8ff265b..01ce6cc 100644
--- a/ntcore/src/main/native/include/networktables/NTSendableBuilder.h
+++ b/ntcore/src/main/native/include/networktables/NTSendableBuilder.h
@@ -4,15 +4,14 @@
#pragma once
-#include <functional>
#include <memory>
#include <string_view>
+#include <wpi/FunctionExtras.h>
#include <wpi/sendable/SendableBuilder.h>
#include "networktables/NetworkTable.h"
-#include "networktables/NetworkTableEntry.h"
-#include "networktables/NetworkTableValue.h"
+#include "networktables/Topic.h"
namespace nt {
@@ -26,27 +25,16 @@
*
* @param func function
*/
- virtual void SetUpdateTable(std::function<void()> func) = 0;
+ virtual void SetUpdateTable(wpi::unique_function<void()> func) = 0;
/**
* Add a property without getters or setters. This can be used to get
* entry handles for the function called by SetUpdateTable().
*
* @param key property name
- * @return Network table entry
+ * @return Network table topic
*/
- virtual NetworkTableEntry GetEntry(std::string_view key) = 0;
-
- /**
- * Add a NetworkTableValue property.
- *
- * @param key property name
- * @param getter getter function (returns current value)
- * @param setter setter function (sets new value)
- */
- virtual void AddValueProperty(
- std::string_view key, std::function<std::shared_ptr<Value>()> getter,
- std::function<void(std::shared_ptr<Value>)> setter) = 0;
+ virtual Topic GetTopic(std::string_view key) = 0;
/**
* Get the network table.
diff --git a/ntcore/src/main/native/include/networktables/NetworkTable.h b/ntcore/src/main/native/include/networktables/NetworkTable.h
index 3a04e45..d34f54b 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTable.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTable.h
@@ -2,11 +2,11 @@
// 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.
-#ifndef NTCORE_NETWORKTABLES_NETWORKTABLE_H_
-#define NTCORE_NETWORKTABLES_NETWORKTABLE_H_
+#pragma once
#include <functional>
#include <memory>
+#include <span>
#include <string>
#include <string_view>
#include <utility>
@@ -14,16 +14,25 @@
#include <wpi/StringMap.h>
#include <wpi/mutex.h>
-#include <wpi/span.h>
#include "networktables/NetworkTableEntry.h"
-#include "networktables/TableEntryListener.h"
-#include "networktables/TableListener.h"
#include "ntcore_c.h"
namespace nt {
+class BooleanArrayTopic;
+class BooleanTopic;
+class DoubleArrayTopic;
+class DoubleTopic;
+class FloatArrayTopic;
+class FloatTopic;
+class IntegerArrayTopic;
+class IntegerTopic;
class NetworkTableInstance;
+class RawTopic;
+class StringArrayTopic;
+class StringTopic;
+class Topic;
/**
* @defgroup ntcore_cpp_api ntcore C++ object-oriented API
@@ -41,7 +50,6 @@
std::string m_path;
mutable wpi::mutex m_mutex;
mutable wpi::StringMap<NT_Entry> m_entries;
- std::vector<NT_EntryListener> m_listeners;
struct private_init {};
friend class NetworkTableInstance;
@@ -94,7 +102,7 @@
* instead.
*/
NetworkTable(NT_Inst inst, std::string_view path, const private_init&);
- virtual ~NetworkTable();
+ ~NetworkTable();
/**
* Gets the instance for the table.
@@ -117,52 +125,100 @@
NetworkTableEntry GetEntry(std::string_view key) const;
/**
- * Listen to keys only within this table.
+ * Get (generic) topic.
*
- * @param listener listener to add
- * @param flags EntryListenerFlags bitmask
- * @return Listener handle
+ * @param name topic name
+ * @return Topic
*/
- NT_EntryListener AddEntryListener(TableEntryListener listener,
- unsigned int flags) const;
+ Topic GetTopic(std::string_view name) const;
/**
- * Listen to a single key.
+ * Get boolean topic.
*
- * @param key the key name
- * @param listener listener to add
- * @param flags EntryListenerFlags bitmask
- * @return Listener handle
+ * @param name topic name
+ * @return BooleanTopic
*/
- NT_EntryListener AddEntryListener(std::string_view key,
- TableEntryListener listener,
- unsigned int flags) const;
+ BooleanTopic GetBooleanTopic(std::string_view name) const;
/**
- * Remove an entry listener.
+ * Get integer topic.
*
- * @param listener listener handle
+ * @param name topic name
+ * @return IntegerTopic
*/
- void RemoveEntryListener(NT_EntryListener listener) const;
+ IntegerTopic GetIntegerTopic(std::string_view name) const;
/**
- * Listen for sub-table creation.
- * This calls the listener once for each newly created sub-table.
- * It immediately calls the listener for any existing sub-tables.
+ * Get float topic.
*
- * @param listener listener to add
- * @param localNotify notify local changes as well as remote
- * @return Listener handle
+ * @param name topic name
+ * @return FloatTopic
*/
- NT_EntryListener AddSubTableListener(TableListener listener,
- bool localNotify = false);
+ FloatTopic GetFloatTopic(std::string_view name) const;
/**
- * Remove a sub-table listener.
+ * Get double topic.
*
- * @param listener listener handle
+ * @param name topic name
+ * @return DoubleTopic
*/
- void RemoveTableListener(NT_EntryListener listener);
+ DoubleTopic GetDoubleTopic(std::string_view name) const;
+
+ /**
+ * Get String topic.
+ *
+ * @param name topic name
+ * @return StringTopic
+ */
+ StringTopic GetStringTopic(std::string_view name) const;
+
+ /**
+ * Get raw topic.
+ *
+ * @param name topic name
+ * @return BooleanArrayTopic
+ */
+ RawTopic GetRawTopic(std::string_view name) const;
+
+ /**
+ * Get boolean[] topic.
+ *
+ * @param name topic name
+ * @return BooleanArrayTopic
+ */
+ BooleanArrayTopic GetBooleanArrayTopic(std::string_view name) const;
+
+ /**
+ * Get integer[] topic.
+ *
+ * @param name topic name
+ * @return IntegerArrayTopic
+ */
+ IntegerArrayTopic GetIntegerArrayTopic(std::string_view name) const;
+
+ /**
+ * Get float[] topic.
+ *
+ * @param name topic name
+ * @return FloatArrayTopic
+ */
+ FloatArrayTopic GetFloatArrayTopic(std::string_view name) const;
+
+ /**
+ * Get double[] topic.
+ *
+ * @param name topic name
+ * @return DoubleArrayTopic
+ */
+ DoubleArrayTopic GetDoubleArrayTopic(std::string_view name) const;
+
+ /**
+ * Get String[] topic.
+ *
+ * @param name topic name
+ * @return StringArrayTopic
+ */
+ StringArrayTopic GetStringArrayTopic(std::string_view name) const;
/**
* Returns the table at the specified key. If there is no table at the
@@ -192,6 +248,23 @@
bool ContainsSubTable(std::string_view key) const;
/**
+ * Gets topic information for all keys in the table (not including
+ * sub-tables).
+ *
+ * @param types bitmask of types; 0 is treated as a "don't care".
+ * @return topic information for keys currently in the table
+ */
+ std::vector<TopicInfo> GetTopicInfo(int types = 0) const;
+
+ /**
+ * Gets all topics in the table (not including sub-tables).
+ *
+ * @param types bitmask of types; 0 is treated as a "don't care".
+ * @return topic for keys currently in the table
+ */
+ std::vector<Topic> GetTopics(int types = 0) const;
+
+ /**
* Gets all keys in the table (not including sub-tables).
*
* @param types bitmask of types; 0 is treated as a "don't care".
@@ -230,39 +303,6 @@
bool IsPersistent(std::string_view key) const;
/**
- * Sets flags on the specified key in this table. The key can
- * not be null.
- *
- * @param key the key name
- * @param flags the flags to set (bitmask)
- */
- void SetFlags(std::string_view key, unsigned int flags);
-
- /**
- * Clears flags on the specified key in this table. The key can
- * not be null.
- *
- * @param key the key name
- * @param flags the flags to clear (bitmask)
- */
- void ClearFlags(std::string_view key, unsigned int flags);
-
- /**
- * Returns the flags for the specified key.
- *
- * @param key the key name
- * @return the flags, or 0 if the key is not defined
- */
- unsigned int GetFlags(std::string_view key) const;
-
- /**
- * Deletes the specified key in this table.
- *
- * @param key the key name
- */
- void Delete(std::string_view key);
-
- /**
* Put a number in the table
*
* @param key the key to be assigned to
@@ -360,7 +400,7 @@
* std::vector<bool> is special-cased in C++. 0 is false, any
* non-zero value is true.
*/
- bool PutBooleanArray(std::string_view key, wpi::span<const int> value);
+ bool PutBooleanArray(std::string_view key, std::span<const int> value);
/**
* Gets the current value in the table, setting it if it does not exist.
@@ -370,7 +410,7 @@
* @return False if the table key exists with a different type
*/
bool SetDefaultBooleanArray(std::string_view key,
- wpi::span<const int> defaultValue);
+ std::span<const int> defaultValue);
/**
* Returns the boolean array the key maps to. If the key does not exist or is
@@ -389,7 +429,7 @@
* non-zero value is true.
*/
std::vector<int> GetBooleanArray(std::string_view key,
- wpi::span<const int> defaultValue) const;
+ std::span<const int> defaultValue) const;
/**
* Put a number array in the table
@@ -398,7 +438,7 @@
* @param value the value that will be assigned
* @return False if the table key already exists with a different type
*/
- bool PutNumberArray(std::string_view key, wpi::span<const double> value);
+ bool PutNumberArray(std::string_view key, std::span<const double> value);
/**
* Gets the current value in the table, setting it if it does not exist.
@@ -408,7 +448,7 @@
* @returns False if the table key exists with a different type
*/
bool SetDefaultNumberArray(std::string_view key,
- wpi::span<const double> defaultValue);
+ std::span<const double> defaultValue);
/**
* Returns the number array the key maps to. If the key does not exist or is
@@ -423,7 +463,7 @@
* concern, use GetValue() instead.
*/
std::vector<double> GetNumberArray(
- std::string_view key, wpi::span<const double> defaultValue) const;
+ std::string_view key, std::span<const double> defaultValue) const;
/**
* Put a string array in the table
@@ -432,7 +472,7 @@
* @param value the value that will be assigned
* @return False if the table key already exists with a different type
*/
- bool PutStringArray(std::string_view key, wpi::span<const std::string> value);
+ bool PutStringArray(std::string_view key, std::span<const std::string> value);
/**
* Gets the current value in the table, setting it if it does not exist.
@@ -442,7 +482,7 @@
* @returns False if the table key exists with a different type
*/
bool SetDefaultStringArray(std::string_view key,
- wpi::span<const std::string> defaultValue);
+ std::span<const std::string> defaultValue);
/**
* Returns the string array the key maps to. If the key does not exist or is
@@ -457,7 +497,7 @@
* concern, use GetValue() instead.
*/
std::vector<std::string> GetStringArray(
- std::string_view key, wpi::span<const std::string> defaultValue) const;
+ std::string_view key, std::span<const std::string> defaultValue) const;
/**
* Put a raw value (byte array) in the table
@@ -466,7 +506,7 @@
* @param value the value that will be assigned
* @return False if the table key already exists with a different type
*/
- bool PutRaw(std::string_view key, std::string_view value);
+ bool PutRaw(std::string_view key, std::span<const uint8_t> value);
/**
* Gets the current value in the table, setting it if it does not exist.
@@ -475,7 +515,8 @@
* @param defaultValue the default value to set if key doesn't exist.
* @return False if the table key exists with a different type
*/
- bool SetDefaultRaw(std::string_view key, std::string_view defaultValue);
+ bool SetDefaultRaw(std::string_view key,
+ std::span<const uint8_t> defaultValue);
/**
* Returns the raw value (byte array) the key maps to. If the key does not
@@ -489,7 +530,8 @@
* @note This makes a copy of the raw contents. If the overhead of this is a
* concern, use GetValue() instead.
*/
- std::string GetRaw(std::string_view key, std::string_view defaultValue) const;
+ std::vector<uint8_t> GetRaw(std::string_view key,
+ std::span<const uint8_t> defaultValue) const;
/**
* Put a value in the table
@@ -498,7 +540,7 @@
* @param value the value that will be assigned
* @return False if the table key already exists with a different type
*/
- bool PutValue(std::string_view key, std::shared_ptr<Value> value);
+ bool PutValue(std::string_view key, const Value& value);
/**
* Gets the current value in the table, setting it if it does not exist.
@@ -507,8 +549,7 @@
* @param defaultValue the default value to set if key doesn't exist.
* @return False if the table key exists with a different type
*/
- bool SetDefaultValue(std::string_view key,
- std::shared_ptr<Value> defaultValue);
+ bool SetDefaultValue(std::string_view key, const Value& defaultValue);
/**
* Gets the value associated with a key as an object
@@ -517,7 +558,7 @@
* @return the value associated with the given key, or nullptr if the key
* does not exist
*/
- std::shared_ptr<Value> GetValue(std::string_view key) const;
+ Value GetValue(std::string_view key) const;
/**
* Gets the full path of this table. Does not include the trailing "/".
@@ -527,27 +568,62 @@
std::string_view GetPath() const;
/**
- * Save table values to a file. The file format used is identical to
- * that used for SavePersistent.
+ * Called when an event occurs on a topic in a {@link NetworkTable}.
*
- * @param filename filename
- * @return error string, or nullptr if successful
+ * @param table the table the topic exists in
+ * @param key the key associated with the topic that changed
+ * @param event the event
*/
- const char* SaveEntries(std::string_view filename) const;
+ using TableEventListener = std::function<void(
+ NetworkTable* table, std::string_view key, const Event& event)>;
/**
- * Load table values from a file. The file format used is identical to
- * that used for SavePersistent / LoadPersistent.
+ * Listen to topics only within this table.
*
- * @param filename filename
- * @param warn callback function for warnings
- * @return error string, or nullptr if successful
+ * @param eventMask Bitmask of EventFlags values
+ * @param listener listener to add
+ * @return Listener handle
*/
- const char* LoadEntries(
- std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn);
+ NT_Listener AddListener(int eventMask, TableEventListener listener);
+
+ /**
+ * Listen to a single key.
+ *
+ * @param key the key name
+ * @param eventMask Bitmask of EventFlags values
+ * @param listener listener to add
+ * @return Listener handle
+ */
+ NT_Listener AddListener(std::string_view key, int eventMask,
+ TableEventListener listener);
+
+ /**
+ * Called when a new table is created within a NetworkTable.
+ *
+ * @param parent the parent of the table
+ * @param name the name of the new table
+ * @param table the new table
+ */
+ using SubTableListener =
+ std::function<void(NetworkTable* parent, std::string_view name,
+ std::shared_ptr<NetworkTable> table)>;
+
+ /**
+ * Listen for sub-table creation. This calls the listener once for each newly
+ * created sub-table. It immediately calls the listener for any existing
+ * sub-tables.
+ *
+ * @param listener listener to add
+ * @return Listener handle
+ */
+ NT_Listener AddSubTableListener(SubTableListener listener);
+
+ /**
+ * Remove a listener.
+ *
+ * @param listener listener handle
+ */
+ void RemoveListener(NT_Listener listener);
};
} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_NETWORKTABLE_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableEntry.h b/ntcore/src/main/native/include/networktables/NetworkTableEntry.h
index c6c9c86..b9d509a 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableEntry.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTableEntry.h
@@ -2,37 +2,42 @@
// 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.
-#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_H_
-#define NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_H_
+#pragma once
#include <stdint.h>
#include <initializer_list>
#include <memory>
+#include <span>
#include <string>
#include <string_view>
#include <vector>
-#include <wpi/span.h>
+#include <wpi/deprecated.h>
#include "networktables/NetworkTableType.h"
#include "networktables/NetworkTableValue.h"
-#include "networktables/RpcCall.h"
#include "ntcore_c.h"
#include "ntcore_cpp.h"
namespace nt {
class NetworkTableInstance;
+class Topic;
/**
* NetworkTables Entry
+ *
+ * @note For backwards compatibility, the NetworkTableEntry destructor does not
+ * release the entry.
+ *
* @ingroup ntcore_cpp_api
*/
class NetworkTableEntry final {
public:
/**
* Flag values (as returned by GetFlags()).
+ * @deprecated Use IsPersistent() instead.
*/
enum Flags { kPersistent = NT_PERSISTENT };
@@ -94,7 +99,9 @@
* Returns the flags.
*
* @return the flags (bitmask)
+ * @deprecated Use IsPersistent() or topic properties instead
*/
+ WPI_DEPRECATED("Use IsPersistent() or topic properties instead")
unsigned int GetFlags() const;
/**
@@ -102,21 +109,15 @@
*
* @return Entry last change time
*/
- uint64_t GetLastChange() const;
+ int64_t GetLastChange() const;
/**
- * Gets combined information about the entry.
+ * Gets the entry's value. If the entry does not exist, returns an empty
+ * value.
*
- * @return Entry information
+ * @return the entry's value or an empty value if it does not exist.
*/
- EntryInfo GetInfo() const;
-
- /**
- * Gets the entry's value. If the entry does not exist, returns nullptr.
- *
- * @return the entry's value or nullptr if it does not exist.
- */
- std::shared_ptr<Value> GetValue() const;
+ Value GetValue() const;
/**
* Gets the entry's value as a boolean. If the entry does not exist or is of
@@ -128,6 +129,24 @@
bool GetBoolean(bool defaultValue) const;
/**
+ * Gets the entry's value as a integer. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ int64_t GetInteger(int64_t defaultValue) const;
+
+ /**
+ * Gets the entry's value as a float. If the entry does not exist or is of
+ * different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ */
+ float GetFloat(float defaultValue) const;
+
+ /**
* Gets the entry's value as a double. If the entry does not exist or is of
* different type, it will return the default value.
*
@@ -152,7 +171,7 @@
* @param defaultValue the value to be returned if no value is found
* @return the entry's value or the given default value
*/
- std::string GetRaw(std::string_view defaultValue) const;
+ std::vector<uint8_t> GetRaw(std::span<const uint8_t> defaultValue) const;
/**
* Gets the entry's value as a boolean array. If the entry does not exist
@@ -168,10 +187,10 @@
* because std::vector<bool> is special-cased in C++. 0 is false, any
* non-zero value is true.
*/
- std::vector<int> GetBooleanArray(wpi::span<const int> defaultValue) const;
+ std::vector<int> GetBooleanArray(std::span<const int> defaultValue) const;
/**
- * Gets the entry's value as a boolean array. If the entry does not exist
+ * Gets the entry's value as a integer array. If the entry does not exist
* or is of different type, it will return the default value.
*
* @param defaultValue the value to be returned if no value is found
@@ -179,13 +198,21 @@
*
* @note This makes a copy of the array. If the overhead of this is a
* concern, use GetValue() instead.
- *
- * @note The returned array is std::vector<int> instead of std::vector<bool>
- * because std::vector<bool> is special-cased in C++. 0 is false, any
- * non-zero value is true.
*/
- std::vector<int> GetBooleanArray(
- std::initializer_list<int> defaultValue) const;
+ std::vector<int64_t> GetIntegerArray(
+ std::span<const int64_t> defaultValue) const;
+
+ /**
+ * Gets the entry's value as a float array. If the entry does not exist
+ * or is of different type, it will return the default value.
+ *
+ * @param defaultValue the value to be returned if no value is found
+ * @return the entry's value or the given default value
+ *
+ * @note This makes a copy of the array. If the overhead of this is a
+ * concern, use GetValue() instead.
+ */
+ std::vector<float> GetFloatArray(std::span<const float> defaultValue) const;
/**
* Gets the entry's value as a double array. If the entry does not exist
@@ -198,20 +225,7 @@
* concern, use GetValue() instead.
*/
std::vector<double> GetDoubleArray(
- wpi::span<const double> defaultValue) const;
-
- /**
- * Gets the entry's value as a double array. If the entry does not exist
- * or is of different type, it will return the default value.
- *
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
- *
- * @note This makes a copy of the array. If the overhead of this is a
- * concern, use GetValue() instead.
- */
- std::vector<double> GetDoubleArray(
- std::initializer_list<double> defaultValue) const;
+ std::span<const double> defaultValue) const;
/**
* Gets the entry's value as a string array. If the entry does not exist
@@ -224,28 +238,25 @@
* concern, use GetValue() instead.
*/
std::vector<std::string> GetStringArray(
- wpi::span<const std::string> defaultValue) const;
+ std::span<const std::string> defaultValue) const;
/**
- * Gets the entry's value as a string array. If the entry does not exist
- * or is of different type, it will return the default value.
+ * Get an array of all value changes since the last call to ReadQueue.
*
- * @param defaultValue the value to be returned if no value is found
- * @return the entry's value or the given default value
+ * The "poll storage" subscribe option can be used to set the queue depth.
*
- * @note This makes a copy of the array. If the overhead of this is a
- * concern, use GetValue() instead.
+ * @return Array of values; empty array if no new changes have been
+ * published since the previous call.
*/
- std::vector<std::string> GetStringArray(
- std::initializer_list<std::string> defaultValue) const;
+ std::vector<NetworkTableValue> ReadQueue();
/**
* Sets the entry's value if it does not exist.
*
- * @param value the default value to set
+ * @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
- bool SetDefaultValue(std::shared_ptr<Value> value);
+ bool SetDefaultValue(const Value& defaultValue);
/**
* Sets the entry's value if it does not exist.
@@ -261,6 +272,22 @@
* @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
+ bool SetDefaultInteger(int64_t defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
+ bool SetDefaultFloat(float defaultValue);
+
+ /**
+ * Sets the entry's value if it does not exist.
+ *
+ * @param defaultValue the default value to set
+ * @return False if the entry exists with a different type
+ */
bool SetDefaultDouble(double defaultValue);
/**
@@ -277,7 +304,7 @@
* @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
- bool SetDefaultRaw(std::string_view defaultValue);
+ bool SetDefaultRaw(std::span<const uint8_t> defaultValue);
/**
* Sets the entry's value if it does not exist.
@@ -285,7 +312,7 @@
* @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
- bool SetDefaultBooleanArray(wpi::span<const int> defaultValue);
+ bool SetDefaultBooleanArray(std::span<const int> defaultValue);
/**
* Sets the entry's value if it does not exist.
@@ -293,7 +320,7 @@
* @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
- bool SetDefaultBooleanArray(std::initializer_list<int> defaultValue);
+ bool SetDefaultIntegerArray(std::span<const int64_t> defaultValue);
/**
* Sets the entry's value if it does not exist.
@@ -301,7 +328,7 @@
* @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
- bool SetDefaultDoubleArray(wpi::span<const double> defaultValue);
+ bool SetDefaultFloatArray(std::span<const float> defaultValue);
/**
* Sets the entry's value if it does not exist.
@@ -309,7 +336,7 @@
* @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
- bool SetDefaultDoubleArray(std::initializer_list<double> defaultValue);
+ bool SetDefaultDoubleArray(std::span<const double> defaultValue);
/**
* Sets the entry's value if it does not exist.
@@ -317,15 +344,7 @@
* @param defaultValue the default value to set
* @return False if the entry exists with a different type
*/
- bool SetDefaultStringArray(wpi::span<const std::string> defaultValue);
-
- /**
- * Sets the entry's value if it does not exist.
- *
- * @param defaultValue the default value to set
- * @return False if the entry exists with a different type
- */
- bool SetDefaultStringArray(std::initializer_list<std::string> defaultValue);
+ bool SetDefaultStringArray(std::span<const std::string> defaultValue);
/**
* Sets the entry's value.
@@ -333,220 +352,132 @@
* @param value the value to set
* @return False if the entry exists with a different type
*/
- bool SetValue(std::shared_ptr<Value> value);
+ bool SetValue(const Value& value);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetBoolean(bool value);
+ bool SetBoolean(bool value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetDouble(double value);
+ bool SetInteger(int64_t value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetString(std::string_view value);
+ bool SetFloat(float value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetRaw(std::string_view value);
+ bool SetDouble(double value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetBooleanArray(wpi::span<const bool> value);
+ bool SetString(std::string_view value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetBooleanArray(std::initializer_list<bool> value);
+ bool SetRaw(std::span<const uint8_t> value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetBooleanArray(wpi::span<const int> value);
+ bool SetBooleanArray(std::span<const bool> value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetBooleanArray(std::initializer_list<int> value);
+ bool SetBooleanArray(std::span<const int> value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetDoubleArray(wpi::span<const double> value);
+ bool SetIntegerArray(std::span<const int64_t> value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetDoubleArray(std::initializer_list<double> value);
+ bool SetFloatArray(std::span<const float> value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetStringArray(wpi::span<const std::string> value);
+ bool SetDoubleArray(std::span<const double> value, int64_t time = 0);
/**
* Sets the entry's value.
*
* @param value the value to set
+ * @param time the timestamp to set (0 = nt::Now())
* @return False if the entry exists with a different type
*/
- bool SetStringArray(std::initializer_list<std::string> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetValue(std::shared_ptr<Value> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetBoolean(bool value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetDouble(double value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetString(std::string_view value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetRaw(std::string_view value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetBooleanArray(wpi::span<const bool> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetBooleanArray(std::initializer_list<bool> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetBooleanArray(wpi::span<const int> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetBooleanArray(std::initializer_list<int> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetDoubleArray(wpi::span<const double> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetDoubleArray(std::initializer_list<double> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetStringArray(wpi::span<const std::string> value);
-
- /**
- * Sets the entry's value. If the value is of different type, the type is
- * changed to match the new value.
- *
- * @param value the value to set
- */
- void ForceSetStringArray(std::initializer_list<std::string> value);
+ bool SetStringArray(std::span<const std::string> value, int64_t time = 0);
/**
* Sets flags.
*
* @param flags the flags to set (bitmask)
+ * @deprecated Use SetPersistent() or topic properties instead
*/
+ WPI_DEPRECATED("Use SetPersistent() or topic properties instead")
void SetFlags(unsigned int flags);
/**
* Clears flags.
*
* @param flags the flags to clear (bitmask)
+ * @deprecated Use SetPersistent() or topic properties instead
*/
+ WPI_DEPRECATED("Use SetPersistent() or topic properties instead")
void ClearFlags(unsigned int flags);
/**
@@ -567,68 +498,29 @@
bool IsPersistent() const;
/**
- * Deletes the entry.
+ * Stops publishing the entry if it's been published.
*/
+ void Unpublish();
+
+ /**
+ * Deletes the entry.
+ * @deprecated Use Unpublish() instead.
+ */
+ WPI_DEPRECATED("Use Unpublish() instead")
void Delete();
/**
- * Create a callback-based RPC entry point. Only valid to use on the server.
- * The callback function will be called when the RPC is called.
- * This function creates RPC version 0 definitions (raw data in and out).
+ * Gets the entry's topic.
*
- * @param callback callback function
+ * @return Topic
*/
- void CreateRpc(std::function<void(const RpcAnswer& answer)> callback);
-
- /**
- * Create a polled RPC entry point. Only valid to use on the server.
- * The caller is responsible for calling NetworkTableInstance::PollRpc()
- * to poll for servicing incoming RPC calls.
- * This function creates RPC version 0 definitions (raw data in and out).
- */
- void CreatePolledRpc();
-
- /**
- * Call a RPC function. May be used on either the client or server.
- * This function is non-blocking. Either RpcCall::GetResult() or
- * RpcCall::CancelResult() must be called on the return value to either
- * get or ignore the result of the call.
- *
- * @param params parameter
- * @return RPC call object.
- */
- RpcCall CallRpc(std::string_view params);
-
- /**
- * Add a listener for changes to this entry.
- *
- * @param callback listener to add
- * @param flags NotifyKind bitmask
- * @return Listener handle
- */
- NT_EntryListener AddListener(
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const;
-
- /**
- * Remove an entry listener.
- *
- * @param entry_listener Listener handle to remove
- */
- void RemoveListener(NT_EntryListener entry_listener);
+ Topic GetTopic() const;
/**
* Equality operator. Returns true if both instances refer to the same
* native handle.
*/
- bool operator==(const NetworkTableEntry& oth) const {
- return m_handle == oth.m_handle;
- }
-
- /** Inequality operator. */
- bool operator!=(const NetworkTableEntry& oth) const {
- return !(*this == oth);
- }
+ bool operator==(const NetworkTableEntry&) const = default;
protected:
/* Native handle */
@@ -638,5 +530,3 @@
} // namespace nt
#include "networktables/NetworkTableEntry.inc"
-
-#endif // NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableEntry.inc b/ntcore/src/main/native/include/networktables/NetworkTableEntry.inc
index e7deb51..0b4f628 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableEntry.inc
+++ b/ntcore/src/main/native/include/networktables/NetworkTableEntry.inc
@@ -2,8 +2,7 @@
// 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.
-#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_INC_
-#define NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_INC_
+#pragma once
#include <memory>
#include <string>
@@ -11,6 +10,8 @@
#include <vector>
#include "networktables/NetworkTableEntry.h"
+#include "ntcore_cpp.h"
+#include "ntcore_cpp_types.h"
namespace nt {
@@ -39,307 +40,208 @@
return GetEntryFlags(m_handle);
}
-inline uint64_t NetworkTableEntry::GetLastChange() const {
+inline int64_t NetworkTableEntry::GetLastChange() const {
return GetEntryLastChange(m_handle);
}
-inline EntryInfo NetworkTableEntry::GetInfo() const {
- return GetEntryInfo(m_handle);
-}
-
-inline std::shared_ptr<Value> NetworkTableEntry::GetValue() const {
+inline Value NetworkTableEntry::GetValue() const {
return GetEntryValue(m_handle);
}
inline bool NetworkTableEntry::GetBoolean(bool defaultValue) const {
- auto value = GetEntryValue(m_handle);
- if (!value || value->type() != NT_BOOLEAN) {
- return defaultValue;
- }
- return value->GetBoolean();
+ return nt::GetBoolean(m_handle, defaultValue);
+}
+
+inline int64_t NetworkTableEntry::GetInteger(int64_t defaultValue) const {
+ return nt::GetInteger(m_handle, defaultValue);
+}
+
+inline float NetworkTableEntry::GetFloat(float defaultValue) const {
+ return nt::GetFloat(m_handle, defaultValue);
}
inline double NetworkTableEntry::GetDouble(double defaultValue) const {
- auto value = GetEntryValue(m_handle);
- if (!value || value->type() != NT_DOUBLE) {
- return defaultValue;
- }
- return value->GetDouble();
+ return nt::GetDouble(m_handle, defaultValue);
}
inline std::string NetworkTableEntry::GetString(
std::string_view defaultValue) const {
- auto value = GetEntryValue(m_handle);
- if (!value || value->type() != NT_STRING) {
- return std::string{defaultValue};
- }
- return std::string{value->GetString()};
+ return nt::GetString(m_handle, defaultValue);
}
-inline std::string NetworkTableEntry::GetRaw(
- std::string_view defaultValue) const {
- auto value = GetEntryValue(m_handle);
- if (!value || value->type() != NT_RAW) {
- return std::string{defaultValue};
- }
- return std::string{value->GetRaw()};
+inline std::vector<uint8_t> NetworkTableEntry::GetRaw(
+ std::span<const uint8_t> defaultValue) const {
+ return nt::GetRaw(m_handle, defaultValue);
}
inline std::vector<int> NetworkTableEntry::GetBooleanArray(
- wpi::span<const int> defaultValue) const {
- auto value = GetEntryValue(m_handle);
- if (!value || value->type() != NT_BOOLEAN_ARRAY) {
- return {defaultValue.begin(), defaultValue.end()};
- }
- auto arr = value->GetBooleanArray();
- return {arr.begin(), arr.end()};
+ std::span<const int> defaultValue) const {
+ return nt::GetBooleanArray(m_handle, defaultValue);
}
-inline std::vector<int> NetworkTableEntry::GetBooleanArray(
- std::initializer_list<int> defaultValue) const {
- return GetBooleanArray({defaultValue.begin(), defaultValue.end()});
+inline std::vector<int64_t> NetworkTableEntry::GetIntegerArray(
+ std::span<const int64_t> defaultValue) const {
+ return nt::GetIntegerArray(m_handle, defaultValue);
+}
+
+inline std::vector<float> NetworkTableEntry::GetFloatArray(
+ std::span<const float> defaultValue) const {
+ return nt::GetFloatArray(m_handle, defaultValue);
}
inline std::vector<double> NetworkTableEntry::GetDoubleArray(
- wpi::span<const double> defaultValue) const {
- auto value = GetEntryValue(m_handle);
- if (!value || value->type() != NT_DOUBLE_ARRAY) {
- return {defaultValue.begin(), defaultValue.end()};
- }
- auto arr = value->GetDoubleArray();
- return {arr.begin(), arr.end()};
-}
-
-inline std::vector<double> NetworkTableEntry::GetDoubleArray(
- std::initializer_list<double> defaultValue) const {
- return GetDoubleArray({defaultValue.begin(), defaultValue.end()});
+ std::span<const double> defaultValue) const {
+ return nt::GetDoubleArray(m_handle, defaultValue);
}
inline std::vector<std::string> NetworkTableEntry::GetStringArray(
- wpi::span<const std::string> defaultValue) const {
- auto value = GetEntryValue(m_handle);
- if (!value || value->type() != NT_STRING_ARRAY) {
- return {defaultValue.begin(), defaultValue.end()};
- }
- auto arr = value->GetStringArray();
- return {arr.begin(), arr.end()};
+ std::span<const std::string> defaultValue) const {
+ return nt::GetStringArray(m_handle, defaultValue);
}
-inline std::vector<std::string> NetworkTableEntry::GetStringArray(
- std::initializer_list<std::string> defaultValue) const {
- return GetStringArray({defaultValue.begin(), defaultValue.end()});
+inline std::vector<NetworkTableValue> NetworkTableEntry::ReadQueue() {
+ return nt::ReadQueueValue(m_handle);
}
-inline bool NetworkTableEntry::SetDefaultValue(std::shared_ptr<Value> value) {
- return SetDefaultEntryValue(m_handle, value);
+inline bool NetworkTableEntry::SetDefaultValue(const Value& defaultValue) {
+ return SetDefaultEntryValue(m_handle, defaultValue);
}
inline bool NetworkTableEntry::SetDefaultBoolean(bool defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeBoolean(defaultValue));
+ return nt::SetDefaultBoolean(m_handle, defaultValue);
+}
+
+inline bool NetworkTableEntry::SetDefaultInteger(int64_t defaultValue) {
+ return nt::SetDefaultInteger(m_handle, defaultValue);
+}
+
+inline bool NetworkTableEntry::SetDefaultFloat(float defaultValue) {
+ return nt::SetDefaultFloat(m_handle, defaultValue);
}
inline bool NetworkTableEntry::SetDefaultDouble(double defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeDouble(defaultValue));
+ return nt::SetDefaultDouble(m_handle, defaultValue);
}
inline bool NetworkTableEntry::SetDefaultString(std::string_view defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeString(defaultValue));
+ return nt::SetDefaultString(m_handle, defaultValue);
}
-inline bool NetworkTableEntry::SetDefaultRaw(std::string_view defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeRaw(defaultValue));
+inline bool NetworkTableEntry::SetDefaultRaw(
+ std::span<const uint8_t> defaultValue) {
+ return nt::SetDefaultRaw(m_handle, defaultValue);
}
inline bool NetworkTableEntry::SetDefaultBooleanArray(
- wpi::span<const int> defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeBooleanArray(defaultValue));
+ std::span<const int> defaultValue) {
+ return nt::SetDefaultBooleanArray(m_handle, defaultValue);
}
-inline bool NetworkTableEntry::SetDefaultBooleanArray(
- std::initializer_list<int> defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeBooleanArray(defaultValue));
+inline bool NetworkTableEntry::SetDefaultIntegerArray(
+ std::span<const int64_t> defaultValue) {
+ return nt::SetDefaultIntegerArray(m_handle, defaultValue);
+}
+
+inline bool NetworkTableEntry::SetDefaultFloatArray(
+ std::span<const float> defaultValue) {
+ return nt::SetDefaultFloatArray(m_handle, defaultValue);
}
inline bool NetworkTableEntry::SetDefaultDoubleArray(
- wpi::span<const double> defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeDoubleArray(defaultValue));
-}
-
-inline bool NetworkTableEntry::SetDefaultDoubleArray(
- std::initializer_list<double> value) {
- return SetDefaultEntryValue(m_handle, Value::MakeDoubleArray(value));
+ std::span<const double> defaultValue) {
+ return nt::SetDefaultDoubleArray(m_handle, defaultValue);
}
inline bool NetworkTableEntry::SetDefaultStringArray(
- wpi::span<const std::string> defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeStringArray(defaultValue));
+ std::span<const std::string> defaultValue) {
+ return nt::SetDefaultStringArray(m_handle, defaultValue);
}
-inline bool NetworkTableEntry::SetDefaultStringArray(
- std::initializer_list<std::string> defaultValue) {
- return SetDefaultEntryValue(m_handle, Value::MakeStringArray(defaultValue));
-}
-
-inline bool NetworkTableEntry::SetValue(std::shared_ptr<Value> value) {
+inline bool NetworkTableEntry::SetValue(const Value& value) {
return SetEntryValue(m_handle, value);
}
-inline bool NetworkTableEntry::SetBoolean(bool value) {
- return SetEntryValue(m_handle, Value::MakeBoolean(value));
+inline bool NetworkTableEntry::SetBoolean(bool value, int64_t time) {
+ return nt::SetBoolean(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetDouble(double value) {
- return SetEntryValue(m_handle, Value::MakeDouble(value));
+inline bool NetworkTableEntry::SetInteger(int64_t value, int64_t time) {
+ return nt::SetInteger(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetString(std::string_view value) {
- return SetEntryValue(m_handle, Value::MakeString(value));
+inline bool NetworkTableEntry::SetFloat(float value, int64_t time) {
+ return nt::SetFloat(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetRaw(std::string_view value) {
- return SetEntryValue(m_handle, Value::MakeRaw(value));
+inline bool NetworkTableEntry::SetDouble(double value, int64_t time) {
+ return nt::SetDouble(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetBooleanArray(wpi::span<const bool> value) {
- return SetEntryValue(m_handle, Value::MakeBooleanArray(value));
+inline bool NetworkTableEntry::SetString(std::string_view value, int64_t time) {
+ return nt::SetString(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetBooleanArray(
- std::initializer_list<bool> value) {
- return SetEntryValue(m_handle, Value::MakeBooleanArray(value));
+inline bool NetworkTableEntry::SetRaw(std::span<const uint8_t> value,
+ int64_t time) {
+ return nt::SetRaw(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetBooleanArray(wpi::span<const int> value) {
- return SetEntryValue(m_handle, Value::MakeBooleanArray(value));
+inline bool NetworkTableEntry::SetBooleanArray(std::span<const bool> value,
+ int64_t time) {
+ return SetEntryValue(m_handle, Value::MakeBooleanArray(value, time));
}
-inline bool NetworkTableEntry::SetBooleanArray(
- std::initializer_list<int> value) {
- return SetEntryValue(m_handle, Value::MakeBooleanArray(value));
+inline bool NetworkTableEntry::SetBooleanArray(std::span<const int> value,
+ int64_t time) {
+ return nt::SetBooleanArray(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetDoubleArray(wpi::span<const double> value) {
- return SetEntryValue(m_handle, Value::MakeDoubleArray(value));
+inline bool NetworkTableEntry::SetIntegerArray(std::span<const int64_t> value,
+ int64_t time) {
+ return nt::SetIntegerArray(m_handle, value, time);
}
-inline bool NetworkTableEntry::SetDoubleArray(
- std::initializer_list<double> value) {
- return SetEntryValue(m_handle, Value::MakeDoubleArray(value));
+inline bool NetworkTableEntry::SetFloatArray(std::span<const float> value,
+ int64_t time) {
+ return nt::SetFloatArray(m_handle, value, time);
+}
+
+inline bool NetworkTableEntry::SetDoubleArray(std::span<const double> value,
+ int64_t time) {
+ return nt::SetDoubleArray(m_handle, value, time);
}
inline bool NetworkTableEntry::SetStringArray(
- wpi::span<const std::string> value) {
- return SetEntryValue(m_handle, Value::MakeStringArray(value));
-}
-
-inline bool NetworkTableEntry::SetStringArray(
- std::initializer_list<std::string> value) {
- return SetEntryValue(m_handle, Value::MakeStringArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetValue(std::shared_ptr<Value> value) {
- SetEntryTypeValue(m_handle, value);
-}
-
-inline void NetworkTableEntry::ForceSetBoolean(bool value) {
- SetEntryTypeValue(m_handle, Value::MakeBoolean(value));
-}
-
-inline void NetworkTableEntry::ForceSetDouble(double value) {
- SetEntryTypeValue(m_handle, Value::MakeDouble(value));
-}
-
-inline void NetworkTableEntry::ForceSetString(std::string_view value) {
- SetEntryTypeValue(m_handle, Value::MakeString(value));
-}
-
-inline void NetworkTableEntry::ForceSetRaw(std::string_view value) {
- SetEntryTypeValue(m_handle, Value::MakeRaw(value));
-}
-
-inline void NetworkTableEntry::ForceSetBooleanArray(
- wpi::span<const bool> value) {
- SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetBooleanArray(
- std::initializer_list<bool> value) {
- SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetBooleanArray(
- wpi::span<const int> value) {
- SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetBooleanArray(
- std::initializer_list<int> value) {
- SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetDoubleArray(
- wpi::span<const double> value) {
- SetEntryTypeValue(m_handle, Value::MakeDoubleArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetDoubleArray(
- std::initializer_list<double> value) {
- SetEntryTypeValue(m_handle, Value::MakeDoubleArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetStringArray(
- wpi::span<const std::string> value) {
- SetEntryTypeValue(m_handle, Value::MakeStringArray(value));
-}
-
-inline void NetworkTableEntry::ForceSetStringArray(
- std::initializer_list<std::string> value) {
- SetEntryTypeValue(m_handle, Value::MakeStringArray(value));
+ std::span<const std::string> value, int64_t time) {
+ return nt::SetStringArray(m_handle, value, time);
}
inline void NetworkTableEntry::SetFlags(unsigned int flags) {
- SetEntryFlags(m_handle, GetFlags() | flags);
+ SetEntryFlags(m_handle, GetEntryFlags(m_handle) | flags);
}
inline void NetworkTableEntry::ClearFlags(unsigned int flags) {
- SetEntryFlags(m_handle, GetFlags() & ~flags);
+ SetEntryFlags(m_handle, GetEntryFlags(m_handle) & ~flags);
}
inline void NetworkTableEntry::SetPersistent() {
- SetFlags(kPersistent);
+ nt::SetTopicPersistent(nt::GetTopicFromHandle(m_handle), true);
}
inline void NetworkTableEntry::ClearPersistent() {
- ClearFlags(kPersistent);
+ nt::SetTopicPersistent(nt::GetTopicFromHandle(m_handle), false);
}
inline bool NetworkTableEntry::IsPersistent() const {
- return (GetFlags() & kPersistent) != 0;
+ return nt::GetTopicPersistent(nt::GetTopicFromHandle(m_handle));
+}
+
+inline void NetworkTableEntry::Unpublish() {
+ return nt::Unpublish(m_handle);
}
inline void NetworkTableEntry::Delete() {
- DeleteEntry(m_handle);
-}
-
-inline void NetworkTableEntry::CreateRpc(
- std::function<void(const RpcAnswer& answer)> callback) {
- ::nt::CreateRpc(m_handle, std::string_view("\0", 1), callback);
-}
-
-inline RpcCall NetworkTableEntry::CallRpc(std::string_view params) {
- return RpcCall{m_handle, ::nt::CallRpc(m_handle, params)};
-}
-
-inline NT_EntryListener NetworkTableEntry::AddListener(
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const {
- return AddEntryListener(m_handle, callback, flags);
-}
-
-inline void NetworkTableEntry::RemoveListener(NT_EntryListener entry_listener) {
- RemoveEntryListener(entry_listener);
+ Unpublish();
}
} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_INC_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
index 54b5324..fabc634 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.h
@@ -2,18 +2,17 @@
// 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.
-#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_H_
-#define NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_H_
+#pragma once
#include <functional>
#include <memory>
+#include <optional>
+#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
-#include <wpi/span.h>
-
#include "networktables/NetworkTable.h"
#include "networktables/NetworkTableEntry.h"
#include "ntcore_c.h"
@@ -21,6 +20,21 @@
namespace nt {
+class BooleanArrayTopic;
+class BooleanTopic;
+class DoubleArrayTopic;
+class DoubleTopic;
+class FloatArrayTopic;
+class FloatTopic;
+class IntegerArrayTopic;
+class IntegerTopic;
+class MultiSubscriber;
+class RawTopic;
+class StringArrayTopic;
+class StringTopic;
+class Subscriber;
+class Topic;
+
/**
* NetworkTables Instance.
*
@@ -51,9 +65,8 @@
enum NetworkMode {
kNetModeNone = NT_NET_MODE_NONE,
kNetModeServer = NT_NET_MODE_SERVER,
- kNetModeClient = NT_NET_MODE_CLIENT,
- kNetModeStarting = NT_NET_MODE_STARTING,
- kNetModeFailure = NT_NET_MODE_FAILURE,
+ kNetModeClient3 = NT_NET_MODE_CLIENT3,
+ kNetModeClient4 = NT_NET_MODE_CLIENT4,
kNetModeLocal = NT_NET_MODE_LOCAL
};
@@ -73,9 +86,14 @@
};
/**
- * The default port that network tables operates on.
+ * The default port that network tables operates on for NT3.
*/
- enum { kDefaultPort = NT_DEFAULT_PORT };
+ static constexpr unsigned int kDefaultPort3 = NT_DEFAULT_PORT3;
+
+ /**
+ * The default port that network tables operates on for NT4.
+ */
+ static constexpr unsigned int kDefaultPort4 = NT_DEFAULT_PORT4;
/**
* Construct invalid instance.
@@ -115,7 +133,7 @@
*
* @param inst Instance
*/
- static void Destroy(NetworkTableInstance inst);
+ static void Destroy(NetworkTableInstance& inst);
/**
* Gets the native handle for the entry.
@@ -125,6 +143,204 @@
NT_Inst GetHandle() const;
/**
+ * Gets a "generic" (untyped) topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ Topic GetTopic(std::string_view name) const;
+
+ /**
+ * Gets a boolean topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ BooleanTopic GetBooleanTopic(std::string_view name) const;
+
+ /**
+ * Gets an integer topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ IntegerTopic GetIntegerTopic(std::string_view name) const;
+
+ /**
+ * Gets a float topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ FloatTopic GetFloatTopic(std::string_view name) const;
+
+ /**
+ * Gets a double topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ DoubleTopic GetDoubleTopic(std::string_view name) const;
+
+ /**
+ * Gets a string topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ StringTopic GetStringTopic(std::string_view name) const;
+
+ /**
+ * Gets a raw topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ RawTopic GetRawTopic(std::string_view name) const;
+
+ /**
+ * Gets a boolean array topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ BooleanArrayTopic GetBooleanArrayTopic(std::string_view name) const;
+
+ /**
+ * Gets an integer array topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ IntegerArrayTopic GetIntegerArrayTopic(std::string_view name) const;
+
+ /**
+ * Gets a float array topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ FloatArrayTopic GetFloatArrayTopic(std::string_view name) const;
+
+ /**
+ * Gets a double array topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ DoubleArrayTopic GetDoubleArrayTopic(std::string_view name) const;
+
+ /**
+ * Gets a string array topic.
+ *
+ * @param name topic name
+ * @return Topic
+ */
+ StringArrayTopic GetStringArrayTopic(std::string_view name) const;
+
+ /**
+ * Get Published Topics.
+ *
+ * Returns an array of topics.
+ *
+ * @return Array of topics.
+ */
+ std::vector<Topic> GetTopics();
+
+ /**
+ * Get Published Topics.
+ *
+ * Returns an array of topics. The results are filtered by
+ * string prefix to only return a subset of all topics.
+ *
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @return Array of topics.
+ */
+ std::vector<Topic> GetTopics(std::string_view prefix);
+
+ /**
+ * Get Published Topics.
+ *
+ * Returns an array of topics. The results are filtered by
+ * string prefix and type to only return a subset of all topics.
+ *
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types bitmask of NT_Type values; 0 is treated specially
+ * as a "don't care"
+ * @return Array of topics.
+ */
+ std::vector<Topic> GetTopics(std::string_view prefix, unsigned int types);
+
+ /**
+ * Get Published Topics.
+ *
+ * Returns an array of topics. The results are filtered by
+ * string prefix and type to only return a subset of all topics.
+ *
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types array of type strings
+ * @return Array of topic handles.
+ */
+ std::vector<Topic> GetTopics(std::string_view prefix,
+ std::span<std::string_view> types);
+
+ /**
+ * Get Topic Information about multiple topics.
+ *
+ * Returns an array of topic information (handle, name, type, and properties).
+ *
+ * @return Array of topic information.
+ */
+ std::vector<TopicInfo> GetTopicInfo();
+
+ /**
+ * Get Topic Information about multiple topics.
+ *
+ * Returns an array of topic information (handle, name, type, and properties).
+ * The results are filtered by string prefix to only
+ * return a subset of all topics.
+ *
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @return Array of topic information.
+ */
+ std::vector<TopicInfo> GetTopicInfo(std::string_view prefix);
+
+ /**
+ * Get Topic Information about multiple topics.
+ *
+ * Returns an array of topic information (handle, name, type, and properties).
+ * The results are filtered by string prefix and type to only
+ * return a subset of all topics.
+ *
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types bitmask of NT_Type values; 0 is treated specially
+ * as a "don't care"
+ * @return Array of topic information.
+ */
+ std::vector<TopicInfo> GetTopicInfo(std::string_view prefix,
+ unsigned int types);
+
+ /**
+ * Get Topic Information about multiple topics.
+ *
+ * Returns an array of topic information (handle, name, type, and properties).
+ * The results are filtered by string prefix and type to only
+ * return a subset of all topics.
+ *
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types array of type strings
+ * @return Array of topic information.
+ */
+ std::vector<TopicInfo> GetTopicInfo(std::string_view prefix,
+ std::span<std::string_view> types);
+
+ /**
* Gets the entry for a key.
*
* @param name Key
@@ -133,34 +349,6 @@
NetworkTableEntry GetEntry(std::string_view name);
/**
- * Get entries starting with the given prefix.
- *
- * The results are optionally filtered by string prefix and entry type to
- * only return a subset of all entries.
- *
- * @param prefix entry name required prefix; only entries whose name
- * starts with this string are returned
- * @param types bitmask of types; 0 is treated as a "don't care"
- * @return Array of entries.
- */
- std::vector<NetworkTableEntry> GetEntries(std::string_view prefix,
- unsigned int types);
-
- /**
- * Get information about entries starting with the given prefix.
- *
- * The results are optionally filtered by string prefix and entry type to
- * only return a subset of all entries.
- *
- * @param prefix entry name required prefix; only entries whose name
- * starts with this string are returned
- * @param types bitmask of types; 0 is treated as a "don't care"
- * @return Array of entry information.
- */
- std::vector<EntryInfo> GetEntryInfo(std::string_view prefix,
- unsigned int types) const;
-
- /**
* Gets the table with the specified key.
*
* @param key the key name
@@ -169,103 +357,130 @@
std::shared_ptr<NetworkTable> GetTable(std::string_view key) const;
/**
- * Deletes ALL keys in ALL subtables (except persistent values).
- * Use with caution!
- */
- void DeleteAllEntries();
-
- /**
* @{
- * @name Entry Listener Functions
+ * @name Listener Functions
*/
/**
- * Add a listener for all entries starting with a certain prefix.
+ * Remove a listener.
*
- * @param prefix UTF-8 string prefix
- * @param callback listener to add
- * @param flags EntryListenerFlags bitmask
- * @return Listener handle
+ * @param listener Listener handle to remove
*/
- NT_EntryListener AddEntryListener(
- std::string_view prefix,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags) const;
+ static void RemoveListener(NT_Listener listener);
/**
- * Remove an entry listener.
- *
- * @param entry_listener Listener handle to remove
- */
- static void RemoveEntryListener(NT_EntryListener entry_listener);
-
- /**
- * Wait for the entry listener queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the entry listener
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
- bool WaitForEntryListenerQueue(double timeout);
-
- /** @} */
-
- /**
- * @{
- * @name Connection Listener Functions
- */
-
- /**
- * Add a connection listener.
- *
- * @param callback listener to add
- * @param immediate_notify notify listener of all existing connections
- * @return Listener handle
- */
- NT_ConnectionListener AddConnectionListener(
- std::function<void(const ConnectionNotification& event)> callback,
- bool immediate_notify) const;
-
- /**
- * Remove a connection listener.
- *
- * @param conn_listener Listener handle to remove
- */
- static void RemoveConnectionListener(NT_ConnectionListener conn_listener);
-
- /**
- * Wait for the connection listener queue to be empty. This is primarily
- * useful for deterministic testing. This blocks until either the connection
+ * Wait for the listener queue to be empty. This is primarily
+ * useful for deterministic testing. This blocks until either the
* listener queue is empty (e.g. there are no more events that need to be
* passed along to callbacks or poll queues) or the timeout expires.
*
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
+ * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or
+ * a negative value to block indefinitely
* @return False if timed out, otherwise true.
*/
- bool WaitForConnectionListenerQueue(double timeout);
-
- /** @} */
+ bool WaitForListenerQueue(double timeout);
/**
- * @{
- * @name Remote Procedure Call Functions
- */
-
- /**
- * Wait for the incoming RPC call queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the RPC call
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
+ * Add a connection listener. The callback function is called asynchronously
+ * on a separate thread, so it's important to use synchronization or atomics
+ * when accessing any shared state from the callback function.
*
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
+ * @param immediate_notify notify listener of all existing connections
+ * @param callback listener to add
+ * @return Listener handle
*/
- bool WaitForRpcCallQueue(double timeout);
+ NT_Listener AddConnectionListener(bool immediate_notify,
+ ListenerCallback callback) const;
+
+ /**
+ * Add a time synchronization listener. The callback function is called
+ * asynchronously on a separate thread, so it's important to use
+ * synchronization or atomics when accessing any shared state from the
+ * callback function.
+ *
+ * @param immediate_notify notify listener of current time synchronization
+ * value
+ * @param callback listener to add
+ * @return Listener handle
+ */
+ NT_Listener AddTimeSyncListener(bool immediate_notify,
+ ListenerCallback callback) const;
+
+ /**
+ * Add a listener for changes on a particular topic. The callback
+ * function is called asynchronously on a separate thread, so it's important
+ * to use synchronization or atomics when accessing any shared state from the
+ * callback function.
+ *
+ * This creates a corresponding internal subscriber with the lifetime of the
+ * listener.
+ *
+ * @param topic Topic
+ * @param eventMask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ NT_Listener AddListener(Topic topic, unsigned int eventMask,
+ ListenerCallback listener);
+
+ /**
+ * Add a listener for changes on a subscriber. The callback
+ * function is called asynchronously on a separate thread, so it's important
+ * to use synchronization or atomics when accessing any shared state from the
+ * callback function. This does NOT keep the subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param eventMask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ NT_Listener AddListener(Subscriber& subscriber, unsigned int eventMask,
+ ListenerCallback listener);
+
+ /**
+ * Add a listener for changes on a subscriber. The callback
+ * function is called asynchronously on a separate thread, so it's important
+ * to use synchronization or atomics when accessing any shared state from the
+ * callback function. This does NOT keep the subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param eventMask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ NT_Listener AddListener(MultiSubscriber& subscriber, int eventMask,
+ ListenerCallback listener);
+
+ /**
+ * Add a listener for changes on an entry. The callback function
+ * is called asynchronously on a separate thread, so it's important to use
+ * synchronization or atomics when accessing any shared state from the
+ * callback function.
+ *
+ * @param entry Entry
+ * @param eventMask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ NT_Listener AddListener(const NetworkTableEntry& entry, int eventMask,
+ ListenerCallback listener);
+
+ /**
+ * Add a listener for changes to topics with names that start with any
+ * of the given prefixes. The callback function is called asynchronously on a
+ * separate thread, so it's important to use synchronization or atomics when
+ * accessing any shared state from the callback function.
+ *
+ * This creates a corresponding internal subscriber with the lifetime of the
+ * listener.
+ *
+ * @param prefixes Topic name string prefixes
+ * @param eventMask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener handle
+ */
+ NT_Listener AddListener(std::span<const std::string_view> prefixes,
+ int eventMask, ListenerCallback listener);
/** @} */
@@ -275,16 +490,6 @@
*/
/**
- * Set the network identity of this node.
- *
- * This is the name used during the initial connection handshake, and is
- * visible through ConnectionInfo on the remote node.
- *
- * @param name identity to advertise
- */
- void SetNetworkIdentity(std::string_view name);
-
- /**
* Get the current network mode.
*
* @return Bitmask of NetworkMode.
@@ -311,11 +516,13 @@
* null terminated)
* @param listen_address the address to listen on, or null to listen on any
* address (UTF-8 string, null terminated)
- * @param port port to communicate over
+ * @param port3 port to communicate over (NT3)
+ * @param port4 port to communicate over (NT4)
*/
- void StartServer(std::string_view persist_filename = "networktables.ini",
+ void StartServer(std::string_view persist_filename = "networktables.json",
const char* listen_address = "",
- unsigned int port = kDefaultPort);
+ unsigned int port3 = kDefaultPort3,
+ unsigned int port4 = kDefaultPort4);
/**
* Stops the server if it is running.
@@ -323,45 +530,20 @@
void StopServer();
/**
- * Starts a client. Use SetServer to set the server name and port.
+ * Starts a NT3 client. Use SetServer or SetServerTeam to set the server name
+ * and port.
+ *
+ * @param identity network identity to advertise (cannot be empty string)
*/
- void StartClient();
+ void StartClient3(std::string_view identity);
/**
- * Starts a client using the specified server and port
+ * Starts a NT4 client. Use SetServer or SetServerTeam to set the server name
+ * and port.
*
- * @param server_name server name (UTF-8 string, null terminated)
- * @param port port to communicate over
+ * @param identity network identity to advertise (cannot be empty string)
*/
- void StartClient(const char* server_name, unsigned int port = kDefaultPort);
-
- /**
- * Starts a client using the specified (server, port) combinations. The
- * client will attempt to connect to each server in round robin fashion.
- *
- * @param servers array of server name and port pairs
- */
- void StartClient(
- wpi::span<const std::pair<std::string_view, unsigned int>> servers);
-
- /**
- * Starts a client using the specified servers and port. The
- * client will attempt to connect to each server in round robin fashion.
- *
- * @param servers array of server names
- * @param port port to communicate over
- */
- void StartClient(wpi::span<const std::string_view> servers,
- unsigned int port = kDefaultPort);
-
- /**
- * Starts a client using commonly known robot addresses for the specified
- * team.
- *
- * @param team team number
- * @param port port to communicate over
- */
- void StartClientTeam(unsigned int team, unsigned int port = kDefaultPort);
+ void StartClient4(std::string_view identity);
/**
* Stops the client if it is running.
@@ -372,46 +554,46 @@
* Sets server address and port for client (without restarting client).
*
* @param server_name server name (UTF-8 string, null terminated)
- * @param port port to communicate over
+ * @param port port to communicate over (0 = default)
*/
- void SetServer(const char* server_name, unsigned int port = kDefaultPort);
+ void SetServer(const char* server_name, unsigned int port = 0);
/**
* Sets server addresses and ports for client (without restarting client).
* The client will attempt to connect to each server in round robin fashion.
*
- * @param servers array of server name and port pairs
+ * @param servers array of server address and port pairs
*/
void SetServer(
- wpi::span<const std::pair<std::string_view, unsigned int>> servers);
+ std::span<const std::pair<std::string_view, unsigned int>> servers);
/**
* Sets server addresses and port for client (without restarting client).
* The client will attempt to connect to each server in round robin fashion.
*
* @param servers array of server names
- * @param port port to communicate over
+ * @param port port to communicate over (0 = default)
*/
- void SetServer(wpi::span<const std::string_view> servers,
- unsigned int port = kDefaultPort);
+ void SetServer(std::span<const std::string_view> servers,
+ unsigned int port = 0);
/**
* Sets server addresses and port for client (without restarting client).
* Connects using commonly known robot addresses for the specified team.
*
* @param team team number
- * @param port port to communicate over
+ * @param port port to communicate over (0 = default)
*/
- void SetServerTeam(unsigned int team, unsigned int port = kDefaultPort);
+ void SetServerTeam(unsigned int team, unsigned int port = 0);
/**
* Starts requesting server address from Driver Station.
* This connects to the Driver Station running on localhost to obtain the
* server IP address.
*
- * @param port server port to use in combination with IP from DS
+ * @param port server port to use in combination with IP from DS (0 = default)
*/
- void StartDSClient(unsigned int port = kDefaultPort);
+ void StartDSClient(unsigned int port = 0);
/**
* Stops requesting server address from Driver Station.
@@ -419,12 +601,10 @@
void StopDSClient();
/**
- * Set the periodic update rate.
- * Sets how frequently updates are sent to other nodes over the network.
- *
- * @param interval update interval in seconds (range 0.01 to 1.0)
+ * Flushes all updated values immediately to the local client/server. This
+ * does not flush to the network.
*/
- void SetUpdateRate(double interval);
+ void FlushLocal() const;
/**
* Flushes all updated values immediately to the network.
@@ -449,59 +629,64 @@
*/
bool IsConnected() const;
+ /**
+ * Get the time offset between server time and local time. Add this value to
+ * local time to get the estimated equivalent server time. In server mode,
+ * this always returns 0. In client mode, this returns the time offset only if
+ * the client and server are connected and have exchanged synchronization
+ * messages. Note the time offset may change over time as it is periodically
+ * updated; to receive updates as events, add a listener to the "time sync"
+ * event.
+ *
+ * @return Time offset in microseconds (optional)
+ */
+ std::optional<int64_t> GetServerTimeOffset() const;
+
/** @} */
/**
* @{
- * @name File Save/Load Functions
+ * @name Data Logger Functions
*/
/**
- * Save persistent values to a file. The server automatically does this,
- * but this function provides a way to save persistent values in the same
- * format to a file on either a client or a server.
+ * Starts logging entry changes to a DataLog.
*
- * @param filename filename
- * @return error string, or nullptr if successful
+ * @param log data log object; lifetime must extend until StopEntryDataLog is
+ * called or the instance is destroyed
+ * @param prefix only store entries with names that start with this prefix;
+ * the prefix is not included in the data log entry name
+ * @param logPrefix prefix to add to data log entry names
+ * @return Data logger handle
*/
- const char* SavePersistent(std::string_view filename) const;
+ NT_DataLogger StartEntryDataLog(wpi::log::DataLog& log,
+ std::string_view prefix,
+ std::string_view logPrefix);
/**
- * Load persistent values from a file. The server automatically does this
- * at startup, but this function provides a way to restore persistent values
- * in the same format from a file at any time on either a client or a server.
+ * Stops logging entry changes to a DataLog.
*
- * @param filename filename
- * @param warn callback function for warnings
- * @return error string, or nullptr if successful
+ * @param logger data logger handle
*/
- const char* LoadPersistent(
- std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn);
+ static void StopEntryDataLog(NT_DataLogger logger);
/**
- * Save table values to a file. The file format used is identical to
- * that used for SavePersistent.
+ * Starts logging connection changes to a DataLog.
*
- * @param filename filename
- * @param prefix save only keys starting with this prefix
- * @return error string, or nullptr if successful
+ * @param log data log object; lifetime must extend until
+ * StopConnectionDataLog is called or the instance is destroyed
+ * @param name data log entry name
+ * @return Data logger handle
*/
- const char* SaveEntries(std::string_view filename,
- std::string_view prefix) const;
+ NT_ConnectionDataLogger StartConnectionDataLog(wpi::log::DataLog& log,
+ std::string_view name);
/**
- * Load table values from a file. The file format used is identical to
- * that used for SavePersistent / LoadPersistent.
+ * Stops logging connection changes to a DataLog.
*
- * @param filename filename
- * @param prefix load only keys starting with this prefix
- * @param warn callback function for warnings
- * @return error string, or nullptr if successful
+ * @param logger data logger handle
*/
- const char* LoadEntries(
- std::string_view filename, std::string_view prefix,
- std::function<void(size_t line, const char* msg)> warn);
+ static void StopConnectionDataLog(NT_ConnectionDataLogger logger);
/** @} */
@@ -517,32 +702,13 @@
* log messages with level greater than or equal to minLevel and less than or
* equal to maxLevel; messages outside this range will be silently ignored.
*
- * @param func log callback function
* @param minLevel minimum log level
* @param maxLevel maximum log level
- * @return Logger handle
+ * @param func callback function
+ * @return Listener handle
*/
- NT_Logger AddLogger(std::function<void(const LogMessage& msg)> func,
- unsigned int minLevel, unsigned int maxLevel);
-
- /**
- * Remove a logger.
- *
- * @param logger Logger handle to remove
- */
- static void RemoveLogger(NT_Logger logger);
-
- /**
- * Wait for the incoming log event queue to be empty. This is primarily
- * useful for deterministic testing. This blocks until either the log event
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
- bool WaitForLoggerQueue(double timeout);
+ NT_Listener AddLogger(unsigned int minLevel, unsigned int maxLevel,
+ ListenerCallback func);
/** @} */
@@ -550,14 +716,7 @@
* Equality operator. Returns true if both instances refer to the same
* native handle.
*/
- bool operator==(const NetworkTableInstance& other) const {
- return m_handle == other.m_handle;
- }
-
- /** Inequality operator. */
- bool operator!=(const NetworkTableInstance& other) const {
- return !(*this == other);
- }
+ bool operator==(const NetworkTableInstance&) const = default;
private:
/* Native handle */
@@ -567,5 +726,3 @@
} // namespace nt
#include "networktables/NetworkTableInstance.inc"
-
-#endif // NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc
index 5cb7be0..9b712eb 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc
+++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc
@@ -2,14 +2,15 @@
// 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.
-#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_INC_
-#define NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_INC_
+#pragma once
#include <string_view>
#include <utility>
#include <vector>
#include "networktables/NetworkTableInstance.h"
+#include "networktables/Topic.h"
+#include "ntcore_c.h"
namespace nt {
@@ -26,9 +27,10 @@
return NetworkTableInstance{CreateInstance()};
}
-inline void NetworkTableInstance::Destroy(NetworkTableInstance inst) {
+inline void NetworkTableInstance::Destroy(NetworkTableInstance& inst) {
if (inst.m_handle != 0) {
DestroyInstance(inst.m_handle);
+ inst.m_handle = 0;
}
}
@@ -36,53 +38,79 @@
return m_handle;
}
+inline std::vector<Topic> NetworkTableInstance::GetTopics() {
+ auto handles = ::nt::GetTopics(m_handle, "", 0);
+ return {handles.begin(), handles.end()};
+}
+
+inline std::vector<Topic> NetworkTableInstance::GetTopics(
+ std::string_view prefix) {
+ auto handles = ::nt::GetTopics(m_handle, prefix, 0);
+ return {handles.begin(), handles.end()};
+}
+
+inline std::vector<Topic> NetworkTableInstance::GetTopics(
+ std::string_view prefix, unsigned int types) {
+ auto handles = ::nt::GetTopics(m_handle, prefix, types);
+ return {handles.begin(), handles.end()};
+}
+
+inline std::vector<Topic> NetworkTableInstance::GetTopics(
+ std::string_view prefix, std::span<std::string_view> types) {
+ auto handles = ::nt::GetTopics(m_handle, prefix, types);
+ return {handles.begin(), handles.end()};
+}
+
+inline std::vector<TopicInfo> NetworkTableInstance::GetTopicInfo() {
+ return ::nt::GetTopicInfo(m_handle, "", 0);
+}
+
+inline std::vector<TopicInfo> NetworkTableInstance::GetTopicInfo(
+ std::string_view prefix) {
+ return ::nt::GetTopicInfo(m_handle, prefix, 0);
+}
+
+inline std::vector<TopicInfo> NetworkTableInstance::GetTopicInfo(
+ std::string_view prefix, unsigned int types) {
+ return ::nt::GetTopicInfo(m_handle, prefix, types);
+}
+
+inline std::vector<TopicInfo> NetworkTableInstance::GetTopicInfo(
+ std::string_view prefix, std::span<std::string_view> types) {
+ return ::nt::GetTopicInfo(m_handle, prefix, types);
+}
+
inline NetworkTableEntry NetworkTableInstance::GetEntry(std::string_view name) {
return NetworkTableEntry{::nt::GetEntry(m_handle, name)};
}
-inline std::vector<NetworkTableEntry> NetworkTableInstance::GetEntries(
- std::string_view prefix, unsigned int types) {
- std::vector<NetworkTableEntry> entries;
- for (auto entry : ::nt::GetEntries(m_handle, prefix, types)) {
- entries.emplace_back(entry);
- }
- return entries;
+inline bool NetworkTableInstance::WaitForListenerQueue(double timeout) {
+ return ::nt::WaitForListenerQueue(m_handle, timeout);
}
-inline std::vector<EntryInfo> NetworkTableInstance::GetEntryInfo(
- std::string_view prefix, unsigned int types) const {
- return ::nt::GetEntryInfo(m_handle, prefix, types);
+inline void NetworkTableInstance::RemoveListener(NT_Listener listener) {
+ ::nt::RemoveListener(listener);
}
-inline void NetworkTableInstance::DeleteAllEntries() {
- ::nt::DeleteAllEntries(m_handle);
+inline NT_Listener NetworkTableInstance::AddConnectionListener(
+ bool immediate_notify, ListenerCallback callback) const {
+ return ::nt::AddListener(
+ m_handle,
+ NT_EVENT_CONNECTION | (immediate_notify ? NT_EVENT_IMMEDIATE : 0),
+ std::move(callback));
}
-inline void NetworkTableInstance::RemoveEntryListener(
- NT_EntryListener entry_listener) {
- ::nt::RemoveEntryListener(entry_listener);
+inline NT_Listener NetworkTableInstance::AddTimeSyncListener(
+ bool immediate_notify, ListenerCallback callback) const {
+ return ::nt::AddListener(
+ m_handle, NT_EVENT_TIMESYNC | (immediate_notify ? NT_EVENT_IMMEDIATE : 0),
+ std::move(callback));
}
-inline bool NetworkTableInstance::WaitForEntryListenerQueue(double timeout) {
- return ::nt::WaitForEntryListenerQueue(m_handle, timeout);
-}
-
-inline void NetworkTableInstance::RemoveConnectionListener(
- NT_ConnectionListener conn_listener) {
- ::nt::RemoveConnectionListener(conn_listener);
-}
-
-inline bool NetworkTableInstance::WaitForConnectionListenerQueue(
- double timeout) {
- return ::nt::WaitForConnectionListenerQueue(m_handle, timeout);
-}
-
-inline bool NetworkTableInstance::WaitForRpcCallQueue(double timeout) {
- return ::nt::WaitForRpcCallQueue(m_handle, timeout);
-}
-
-inline void NetworkTableInstance::SetNetworkIdentity(std::string_view name) {
- ::nt::SetNetworkIdentity(m_handle, name);
+inline NT_Listener NetworkTableInstance::AddListener(
+ std::span<const std::string_view> prefixes, int eventMask,
+ ListenerCallback listener) {
+ return ::nt::AddListener(m_handle, prefixes, eventMask, std::move(listener));
}
inline unsigned int NetworkTableInstance::GetNetworkMode() const {
@@ -99,31 +127,21 @@
inline void NetworkTableInstance::StartServer(std::string_view persist_filename,
const char* listen_address,
- unsigned int port) {
- ::nt::StartServer(m_handle, persist_filename, listen_address, port);
+ unsigned int port3,
+ unsigned int port4) {
+ ::nt::StartServer(m_handle, persist_filename, listen_address, port3, port4);
}
inline void NetworkTableInstance::StopServer() {
::nt::StopServer(m_handle);
}
-inline void NetworkTableInstance::StartClient() {
- ::nt::StartClient(m_handle);
+inline void NetworkTableInstance::StartClient3(std::string_view identity) {
+ ::nt::StartClient3(m_handle, identity);
}
-inline void NetworkTableInstance::StartClient(const char* server_name,
- unsigned int port) {
- ::nt::StartClient(m_handle, server_name, port);
-}
-
-inline void NetworkTableInstance::StartClient(
- wpi::span<const std::pair<std::string_view, unsigned int>> servers) {
- ::nt::StartClient(m_handle, servers);
-}
-
-inline void NetworkTableInstance::StartClientTeam(unsigned int team,
- unsigned int port) {
- ::nt::StartClientTeam(m_handle, team, port);
+inline void NetworkTableInstance::StartClient4(std::string_view identity) {
+ ::nt::StartClient4(m_handle, identity);
}
inline void NetworkTableInstance::StopClient() {
@@ -136,7 +154,7 @@
}
inline void NetworkTableInstance::SetServer(
- wpi::span<const std::pair<std::string_view, unsigned int>> servers) {
+ std::span<const std::pair<std::string_view, unsigned int>> servers) {
::nt::SetServer(m_handle, servers);
}
@@ -153,8 +171,8 @@
::nt::StopDSClient(m_handle);
}
-inline void NetworkTableInstance::SetUpdateRate(double interval) {
- ::nt::SetUpdateRate(m_handle, interval);
+inline void NetworkTableInstance::FlushLocal() const {
+ ::nt::FlushLocal(m_handle);
}
inline void NetworkTableInstance::Flush() const {
@@ -170,42 +188,35 @@
return ::nt::IsConnected(m_handle);
}
-inline const char* NetworkTableInstance::SavePersistent(
- std::string_view filename) const {
- return ::nt::SavePersistent(m_handle, filename);
+inline std::optional<int64_t> NetworkTableInstance::GetServerTimeOffset()
+ const {
+ return ::nt::GetServerTimeOffset(m_handle);
}
-inline const char* NetworkTableInstance::LoadPersistent(
- std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn) {
- return ::nt::LoadPersistent(m_handle, filename, warn);
+inline NT_DataLogger NetworkTableInstance::StartEntryDataLog(
+ wpi::log::DataLog& log, std::string_view prefix,
+ std::string_view logPrefix) {
+ return ::nt::StartEntryDataLog(m_handle, log, prefix, logPrefix);
}
-inline const char* NetworkTableInstance::SaveEntries(
- std::string_view filename, std::string_view prefix) const {
- return ::nt::SaveEntries(m_handle, filename, prefix);
+inline void NetworkTableInstance::StopEntryDataLog(NT_DataLogger logger) {
+ ::nt::StopEntryDataLog(logger);
}
-inline const char* NetworkTableInstance::LoadEntries(
- std::string_view filename, std::string_view prefix,
- std::function<void(size_t line, const char* msg)> warn) {
- return ::nt::LoadEntries(m_handle, filename, prefix, warn);
+inline NT_ConnectionDataLogger NetworkTableInstance::StartConnectionDataLog(
+ wpi::log::DataLog& log, std::string_view name) {
+ return ::nt::StartConnectionDataLog(m_handle, log, name);
}
-inline NT_Logger NetworkTableInstance::AddLogger(
- std::function<void(const LogMessage& msg)> func, unsigned int min_level,
- unsigned int max_level) {
- return ::nt::AddLogger(m_handle, func, min_level, max_level);
+inline void NetworkTableInstance::StopConnectionDataLog(
+ NT_ConnectionDataLogger logger) {
+ ::nt::StopConnectionDataLog(logger);
}
-inline void NetworkTableInstance::RemoveLogger(NT_Logger logger) {
- ::nt::RemoveLogger(logger);
-}
-
-inline bool NetworkTableInstance::WaitForLoggerQueue(double timeout) {
- return ::nt::WaitForLoggerQueue(m_handle, timeout);
+inline NT_Listener NetworkTableInstance::AddLogger(unsigned int min_level,
+ unsigned int max_level,
+ ListenerCallback func) {
+ return ::nt::AddLogger(m_handle, min_level, max_level, std::move(func));
}
} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_INC_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableListener.h b/ntcore/src/main/native/include/networktables/NetworkTableListener.h
new file mode 100644
index 0000000..1a8cf1f
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableListener.h
@@ -0,0 +1,312 @@
+// 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 <functional>
+#include <span>
+#include <string_view>
+#include <vector>
+
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+class MultiSubscriber;
+class NetworkTableEntry;
+class NetworkTableInstance;
+class Subscriber;
+class Topic;
+
+/**
+ * Event listener. This calls back to a callback function when an event
+ * matching the specified mask occurs. The callback function is called
+ * asynchronously on a separate thread, so it's important to use synchronization
+ * or atomics when accessing any shared state from the callback function.
+ */
+class NetworkTableListener final {
+ public:
+ NetworkTableListener() = default;
+
+ /**
+ * Create a listener for changes to topics with names that start with any of
+ * the given prefixes. This creates a corresponding internal subscriber with
+ * the lifetime of the listener.
+ *
+ * @param inst Instance
+ * @param prefixes Topic name string prefixes
+ * @param mask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateListener(
+ NetworkTableInstance inst, std::span<const std::string_view> prefixes,
+ unsigned int mask, ListenerCallback listener);
+
+ /**
+ * Create a listener for changes on a particular topic. This creates a
+ * corresponding internal subscriber with the lifetime of the listener.
+ *
+ * @param topic Topic
+ * @param mask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateListener(Topic topic, unsigned int mask,
+ ListenerCallback listener);
+
+ /**
+ * Create a listener for topic changes on a subscriber. This does NOT keep the
+ * subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param mask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateListener(Subscriber& subscriber,
+ unsigned int mask,
+ ListenerCallback listener);
+
+ /**
+ * Create a listener for topic changes on a subscriber. This does NOT keep the
+ * subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param mask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateListener(MultiSubscriber& subscriber,
+ unsigned int mask,
+ ListenerCallback listener);
+
+ /**
+ * Create a listener for topic changes on an entry.
+ *
+ * @param entry Entry
+ * @param mask Bitmask of EventFlags values
+ * @param listener Listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateListener(NetworkTableEntry& entry,
+ unsigned int mask,
+ ListenerCallback listener);
+
+ /**
+ * Create a connection listener.
+ *
+ * @param inst instance
+ * @param immediate_notify notify listener of all existing connections
+ * @param listener listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateConnectionListener(
+ NetworkTableInstance inst, bool immediate_notify,
+ ListenerCallback listener);
+
+ /**
+ * Create a time synchronization listener.
+ *
+ * @param inst instance
+ * @param immediate_notify notify listener of current time synchronization
+ * value
+ * @param listener listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateTimeSyncListener(NetworkTableInstance inst,
+ bool immediate_notify,
+ ListenerCallback listener);
+
+ /**
+ * Create a listener for log messages. By default, log messages are sent to
+ * stderr; this function sends log messages with the specified levels to the
+ * provided callback function instead. The callback function will only be
+ * called for log messages with level greater than or equal to minLevel and
+ * less than or equal to maxLevel; messages outside this range will be
+ * silently ignored.
+ *
+ * @param inst instance
+ * @param minLevel minimum log level
+ * @param maxLevel maximum log level
+ * @param listener listener function
+ * @return Listener
+ */
+ static NetworkTableListener CreateLogger(NetworkTableInstance inst,
+ unsigned int minLevel,
+ unsigned int maxLevel,
+ ListenerCallback listener);
+
+ NetworkTableListener(const NetworkTableListener&) = delete;
+ NetworkTableListener& operator=(const NetworkTableListener&) = delete;
+ NetworkTableListener(NetworkTableListener&& rhs);
+ NetworkTableListener& operator=(NetworkTableListener&& rhs);
+ ~NetworkTableListener();
+
+ explicit operator bool() const { return m_handle != 0; }
+
+ /**
+ * Gets the native handle.
+ *
+ * @return Handle
+ */
+ NT_Listener GetHandle() const { return m_handle; }
+
+ /**
+ * Wait for the listener queue to be empty. This is primarily useful for
+ * deterministic testing. This blocks until either the listener queue is
+ * empty (e.g. there are no more events that need to be passed along to
+ * callbacks or poll queues) or the timeout expires.
+ *
+ * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or
+ * a negative value to block indefinitely
+ * @return False if timed out, otherwise true.
+ */
+ bool WaitForQueue(double timeout);
+
+ private:
+ explicit NetworkTableListener(NT_Listener handle) : m_handle{handle} {}
+
+ NT_Listener m_handle{0};
+};
+
+/**
+ * Event polled listener. This queues events matching the specified mask. Code
+ * using the listener must periodically call ReadQueue() to read the
+ * events.
+ */
+class NetworkTableListenerPoller final {
+ public:
+ NetworkTableListenerPoller() = default;
+
+ /**
+ * Construct a listener poller.
+ *
+ * @param inst Instance
+ */
+ explicit NetworkTableListenerPoller(NetworkTableInstance inst);
+
+ NetworkTableListenerPoller(const NetworkTableListenerPoller&) = delete;
+ NetworkTableListenerPoller& operator=(const NetworkTableListenerPoller&) =
+ delete;
+ NetworkTableListenerPoller(NetworkTableListenerPoller&& rhs);
+ NetworkTableListenerPoller& operator=(NetworkTableListenerPoller&& rhs);
+ ~NetworkTableListenerPoller();
+
+ explicit operator bool() const { return m_handle != 0; }
+
+ /**
+ * Gets the native handle.
+ *
+ * @return Handle
+ */
+ NT_ListenerPoller GetHandle() const { return m_handle; }
+
+ /**
+ * Start listening to topic changes for topics with names that start with any
+ * of the given prefixes. This creates a corresponding internal subscriber
+ * with the lifetime of the listener.
+ *
+ * @param prefixes Topic name string prefixes
+ * @param mask Bitmask of EventFlags values
+ * @return Listener handle
+ */
+ NT_Listener AddListener(std::span<const std::string_view> prefixes,
+ unsigned int mask);
+
+ /**
+ * Start listening to changes to a particular topic. This creates a
+ * corresponding internal subscriber with the lifetime of the listener.
+ *
+ * @param topic Topic
+ * @param mask Bitmask of EventFlags values
+ * @return Listener handle
+ */
+ NT_Listener AddListener(Topic topic, unsigned int mask);
+
+ /**
+ * Start listening to topic changes on a subscriber. This does NOT keep the
+ * subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param mask Bitmask of EventFlags values
+ * @return Listener handle
+ */
+ NT_Listener AddListener(Subscriber& subscriber, unsigned int mask);
+
+ /**
+ * Start listening to topic changes on a subscriber. This does NOT keep the
+ * subscriber active.
+ *
+ * @param subscriber Subscriber
+ * @param mask Bitmask of EventFlags values
+ * @return Listener handle
+ */
+ NT_Listener AddListener(MultiSubscriber& subscriber, unsigned int mask);
+
+ /**
+ * Start listening to topic changes on an entry.
+ *
+ * @param entry Entry
+ * @param mask Bitmask of EventFlags values
+ * @return Listener handle
+ */
+ NT_Listener AddListener(NetworkTableEntry& entry, unsigned int mask);
+
+ /**
+ * Add a connection listener. The callback function is called asynchronously
+ * on a separate thread, so it's important to use synchronization or atomics
+ * when accessing any shared state from the callback function.
+ *
+ * @param immediate_notify notify listener of all existing connections
+ * @return Listener handle
+ */
+ NT_Listener AddConnectionListener(bool immediate_notify);
+
+ /**
+ * Add a time synchronization listener. The callback function is called
+ * asynchronously on a separate thread, so it's important to use
+ * synchronization or atomics when accessing any shared state from the
+ * callback function.
+ *
+ * @param immediate_notify notify listener of current time synchronization
+ * value
+ * @return Listener handle
+ */
+ NT_Listener AddTimeSyncListener(bool immediate_notify);
+
+ /**
+ * Add logger callback function. By default, log messages are sent to stderr;
+ * this function sends log messages with the specified levels to the provided
+ * callback function instead. The callback function will only be called for
+ * log messages with level greater than or equal to minLevel and less than or
+ * equal to maxLevel; messages outside this range will be silently ignored.
+ *
+ * @param minLevel minimum log level
+ * @param maxLevel maximum log level
+ * @return Listener handle
+ */
+ NT_Listener AddLogger(unsigned int minLevel, unsigned int maxLevel);
+
+ /**
+ * Remove a listener.
+ *
+ * @param listener Listener handle
+ */
+ void RemoveListener(NT_Listener listener);
+
+ /**
+ * Read events.
+ *
+ * @return Events since the previous call to ReadQueue()
+ */
+ std::vector<Event> ReadQueue();
+
+ private:
+ NT_ListenerPoller m_handle{0};
+};
+
+} // namespace nt
+
+#include "NetworkTableListener.inc"
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableListener.inc b/ntcore/src/main/native/include/networktables/NetworkTableListener.inc
new file mode 100644
index 0000000..5453d87
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/NetworkTableListener.inc
@@ -0,0 +1,184 @@
+// 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 <span>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "networktables/MultiSubscriber.h"
+#include "networktables/NetworkTableEntry.h"
+#include "networktables/NetworkTableInstance.h"
+#include "networktables/NetworkTableListener.h"
+#include "networktables/Topic.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline NetworkTableListener NetworkTableListener::CreateListener(
+ NetworkTableInstance inst, std::span<const std::string_view> prefixes,
+ unsigned int mask, ListenerCallback listener) {
+ return NetworkTableListener{
+ ::nt::AddListener(inst.GetHandle(), prefixes, mask, std::move(listener))};
+}
+
+inline NetworkTableListener NetworkTableListener::CreateListener(
+ Topic topic, unsigned int mask, ListenerCallback listener) {
+ return NetworkTableListener{
+ nt::AddListener(topic.GetHandle(), mask, std::move(listener))};
+}
+
+inline NetworkTableListener NetworkTableListener::CreateListener(
+ Subscriber& subscriber, unsigned int mask, ListenerCallback listener) {
+ return NetworkTableListener{
+ ::nt::AddListener(subscriber.GetHandle(), mask, std::move(listener))};
+}
+
+inline NetworkTableListener NetworkTableListener::CreateListener(
+ MultiSubscriber& subscriber, unsigned int mask, ListenerCallback listener) {
+ return NetworkTableListener{
+ ::nt::AddListener(subscriber.GetHandle(), mask, std::move(listener))};
+}
+
+inline NetworkTableListener NetworkTableListener::CreateListener(
+ NetworkTableEntry& entry, unsigned int mask, ListenerCallback listener) {
+ return NetworkTableListener{
+ ::nt::AddListener(entry.GetHandle(), mask, std::move(listener))};
+}
+
+inline NetworkTableListener NetworkTableListener::CreateConnectionListener(
+ NetworkTableInstance inst, bool immediate_notify,
+ ListenerCallback listener) {
+ return NetworkTableListener{::nt::AddListener(
+ inst.GetHandle(),
+ NT_EVENT_CONNECTION | (immediate_notify ? NT_EVENT_IMMEDIATE : 0),
+ std::move(listener))};
+}
+
+inline NetworkTableListener NetworkTableListener::CreateTimeSyncListener(
+ NetworkTableInstance inst, bool immediate_notify,
+ ListenerCallback listener) {
+ return NetworkTableListener{::nt::AddListener(
+ inst.GetHandle(),
+ NT_EVENT_TIMESYNC | (immediate_notify ? NT_EVENT_IMMEDIATE : 0),
+ std::move(listener))};
+}
+
+inline NetworkTableListener NetworkTableListener::CreateLogger(
+ NetworkTableInstance inst, unsigned int minLevel, unsigned int maxLevel,
+ ListenerCallback listener) {
+ return NetworkTableListener{::nt::AddLogger(inst.GetHandle(), minLevel,
+ maxLevel, std::move(listener))};
+}
+
+inline NetworkTableListener::NetworkTableListener(NetworkTableListener&& rhs)
+ : m_handle(rhs.m_handle) {
+ rhs.m_handle = 0;
+}
+
+inline NetworkTableListener& NetworkTableListener::operator=(
+ NetworkTableListener&& rhs) {
+ if (m_handle != 0) {
+ nt::RemoveListener(m_handle);
+ }
+ m_handle = rhs.m_handle;
+ rhs.m_handle = 0;
+ return *this;
+}
+
+inline NetworkTableListener::~NetworkTableListener() {
+ if (m_handle != 0) {
+ nt::RemoveListener(m_handle);
+ }
+}
+
+inline bool NetworkTableListener::WaitForQueue(double timeout) {
+ if (m_handle != 0) {
+ return nt::WaitForListenerQueue(m_handle, timeout);
+ } else {
+ return false;
+ }
+}
+
+inline NetworkTableListenerPoller::NetworkTableListenerPoller(
+ NetworkTableInstance inst)
+ : m_handle(nt::CreateListenerPoller(inst.GetHandle())) {}
+
+inline NetworkTableListenerPoller::NetworkTableListenerPoller(
+ NetworkTableListenerPoller&& rhs)
+ : m_handle(rhs.m_handle) {
+ rhs.m_handle = 0;
+}
+
+inline NetworkTableListenerPoller& NetworkTableListenerPoller::operator=(
+ NetworkTableListenerPoller&& rhs) {
+ if (m_handle != 0) {
+ nt::DestroyListenerPoller(m_handle);
+ }
+ m_handle = rhs.m_handle;
+ rhs.m_handle = 0;
+ return *this;
+}
+
+inline NetworkTableListenerPoller::~NetworkTableListenerPoller() {
+ if (m_handle != 0) {
+ nt::DestroyListenerPoller(m_handle);
+ }
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddListener(
+ std::span<const std::string_view> prefixes, unsigned int mask) {
+ return nt::AddPolledListener(m_handle, prefixes, mask);
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddListener(Topic topic,
+ unsigned int mask) {
+ return ::nt::AddPolledListener(m_handle, topic.GetHandle(), mask);
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddListener(
+ Subscriber& subscriber, unsigned int mask) {
+ return ::nt::AddPolledListener(m_handle, subscriber.GetHandle(), mask);
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddListener(
+ MultiSubscriber& subscriber, unsigned int mask) {
+ return ::nt::AddPolledListener(m_handle, subscriber.GetHandle(), mask);
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddListener(
+ NetworkTableEntry& entry, unsigned int mask) {
+ return ::nt::AddPolledListener(m_handle, entry.GetHandle(), mask);
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddConnectionListener(
+ bool immediate_notify) {
+ return ::nt::AddPolledListener(
+ m_handle, ::nt::GetInstanceFromHandle(m_handle),
+ NT_EVENT_CONNECTION | (immediate_notify ? NT_EVENT_IMMEDIATE : 0));
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddTimeSyncListener(
+ bool immediate_notify) {
+ return ::nt::AddPolledListener(
+ m_handle, ::nt::GetInstanceFromHandle(m_handle),
+ NT_EVENT_TIMESYNC | (immediate_notify ? NT_EVENT_IMMEDIATE : 0));
+}
+
+inline NT_Listener NetworkTableListenerPoller::AddLogger(
+ unsigned int minLevel, unsigned int maxLevel) {
+ return ::nt::AddPolledLogger(m_handle, minLevel, maxLevel);
+}
+
+inline void NetworkTableListenerPoller::RemoveListener(NT_Listener listener) {
+ ::nt::RemoveListener(listener);
+}
+
+inline std::vector<Event> NetworkTableListenerPoller::ReadQueue() {
+ return ::nt::ReadListenerQueue(m_handle);
+}
+
+} // namespace nt
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableType.h b/ntcore/src/main/native/include/networktables/NetworkTableType.h
index d7681f2..4b60454 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableType.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTableType.h
@@ -2,8 +2,7 @@
// 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.
-#ifndef NTCORE_NETWORKTABLES_NETWORKTABLETYPE_H_
-#define NTCORE_NETWORKTABLES_NETWORKTABLETYPE_H_
+#pragma once
#include "ntcore_c.h"
@@ -22,9 +21,10 @@
kBooleanArray = NT_BOOLEAN_ARRAY,
kDoubleArray = NT_DOUBLE_ARRAY,
kStringArray = NT_STRING_ARRAY,
- kRpc = NT_RPC
+ kInteger = NT_INTEGER,
+ kFloat = NT_FLOAT,
+ kIntegerArray = NT_INTEGER_ARRAY,
+ kFloatArray = NT_FLOAT_ARRAY
};
} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_NETWORKTABLETYPE_H_
diff --git a/ntcore/src/main/native/include/networktables/NetworkTableValue.h b/ntcore/src/main/native/include/networktables/NetworkTableValue.h
index fcb9cb5..673a833 100644
--- a/ntcore/src/main/native/include/networktables/NetworkTableValue.h
+++ b/ntcore/src/main/native/include/networktables/NetworkTableValue.h
@@ -2,22 +2,20 @@
// 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.
-#ifndef NTCORE_NETWORKTABLES_NETWORKTABLEVALUE_H_
-#define NTCORE_NETWORKTABLES_NETWORKTABLEVALUE_H_
+#pragma once
#include <stdint.h>
#include <cassert>
#include <initializer_list>
#include <memory>
+#include <span>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
-#include <wpi/span.h>
-
#include "ntcore_c.h"
namespace nt {
@@ -31,8 +29,10 @@
public:
Value();
- Value(NT_Type type, uint64_t time, const private_init&);
- ~Value();
+ Value(NT_Type type, int64_t time, const private_init&);
+ Value(NT_Type type, int64_t time, int64_t serverTime, const private_init&);
+
+ explicit operator bool() const { return m_val.type != NT_UNASSIGNED; }
/**
* Get the data type.
@@ -49,18 +49,39 @@
const NT_Value& value() const { return m_val; }
/**
- * Get the creation time of the value.
+ * Get the creation time of the value, in local time.
*
* @return The time, in the units returned by nt::Now().
*/
- uint64_t last_change() const { return m_val.last_change; }
+ int64_t last_change() const { return m_val.last_change; }
/**
- * Get the creation time of the value.
+ * Get the creation time of the value, in local time.
*
* @return The time, in the units returned by nt::Now().
*/
- uint64_t time() const { return m_val.last_change; }
+ int64_t time() const { return m_val.last_change; }
+
+ /**
+ * Set the local creation time of the value.
+ *
+ * @param time The time.
+ */
+ void SetTime(int64_t time) { m_val.last_change = time; }
+
+ /**
+ * Get the creation time of the value, in server time.
+ *
+ * @return The server time.
+ */
+ int64_t server_time() const { return m_val.server_time; }
+
+ /**
+ * Set the creation time of the value, in server time.
+ *
+ * @param time The server time.
+ */
+ void SetServerTime(int64_t time) { m_val.server_time = time; }
/**
* @{
@@ -82,6 +103,20 @@
bool IsBoolean() const { return m_val.type == NT_BOOLEAN; }
/**
+ * Determine if entry value contains an integer.
+ *
+ * @return True if the entry value is of integer type.
+ */
+ bool IsInteger() const { return m_val.type == NT_INTEGER; }
+
+ /**
+ * Determine if entry value contains a float.
+ *
+ * @return True if the entry value is of float type.
+ */
+ bool IsFloat() const { return m_val.type == NT_FLOAT; }
+
+ /**
* Determine if entry value contains a double.
*
* @return True if the entry value is of double type.
@@ -103,13 +138,6 @@
bool IsRaw() const { return m_val.type == NT_RAW; }
/**
- * Determine if entry value contains a rpc definition.
- *
- * @return True if the entry value is of rpc definition type.
- */
- bool IsRpc() const { return m_val.type == NT_RPC; }
-
- /**
* Determine if entry value contains a boolean array.
*
* @return True if the entry value is of boolean array type.
@@ -117,6 +145,20 @@
bool IsBooleanArray() const { return m_val.type == NT_BOOLEAN_ARRAY; }
/**
+ * Determine if entry value contains an integer array.
+ *
+ * @return True if the entry value is of integer array type.
+ */
+ bool IsIntegerArray() const { return m_val.type == NT_INTEGER_ARRAY; }
+
+ /**
+ * Determine if entry value contains a float array.
+ *
+ * @return True if the entry value is of float array type.
+ */
+ bool IsFloatArray() const { return m_val.type == NT_FLOAT_ARRAY; }
+
+ /**
* Determine if entry value contains a double array.
*
* @return True if the entry value is of double array type.
@@ -148,6 +190,26 @@
}
/**
+ * Get the entry's integer value.
+ *
+ * @return The integer value.
+ */
+ int64_t GetInteger() const {
+ assert(m_val.type == NT_INTEGER);
+ return m_val.data.v_int;
+ }
+
+ /**
+ * Get the entry's float value.
+ *
+ * @return The float value.
+ */
+ float GetFloat() const {
+ assert(m_val.type == NT_FLOAT);
+ return m_val.data.v_float;
+ }
+
+ /**
* Get the entry's double value.
*
* @return The double value.
@@ -164,7 +226,7 @@
*/
std::string_view GetString() const {
assert(m_val.type == NT_STRING);
- return m_string;
+ return {m_val.data.v_string.str, m_val.data.v_string.len};
}
/**
@@ -172,19 +234,9 @@
*
* @return The raw value.
*/
- std::string_view GetRaw() const {
+ std::span<const uint8_t> GetRaw() const {
assert(m_val.type == NT_RAW);
- return m_string;
- }
-
- /**
- * Get the entry's rpc definition value.
- *
- * @return The rpc definition value.
- */
- std::string_view GetRpc() const {
- assert(m_val.type == NT_RPC);
- return m_string;
+ return {m_val.data.v_raw.data, m_val.data.v_raw.size};
}
/**
@@ -192,17 +244,37 @@
*
* @return The boolean array value.
*/
- wpi::span<const int> GetBooleanArray() const {
+ std::span<const int> GetBooleanArray() const {
assert(m_val.type == NT_BOOLEAN_ARRAY);
return {m_val.data.arr_boolean.arr, m_val.data.arr_boolean.size};
}
/**
+ * Get the entry's integer array value.
+ *
+ * @return The integer array value.
+ */
+ std::span<const int64_t> GetIntegerArray() const {
+ assert(m_val.type == NT_INTEGER_ARRAY);
+ return {m_val.data.arr_int.arr, m_val.data.arr_int.size};
+ }
+
+ /**
+ * Get the entry's float array value.
+ *
+ * @return The float array value.
+ */
+ std::span<const float> GetFloatArray() const {
+ assert(m_val.type == NT_FLOAT_ARRAY);
+ return {m_val.data.arr_float.arr, m_val.data.arr_float.size};
+ }
+
+ /**
* Get the entry's double array value.
*
* @return The double array value.
*/
- wpi::span<const double> GetDoubleArray() const {
+ std::span<const double> GetDoubleArray() const {
assert(m_val.type == NT_DOUBLE_ARRAY);
return {m_val.data.arr_double.arr, m_val.data.arr_double.size};
}
@@ -212,9 +284,9 @@
*
* @return The string array value.
*/
- wpi::span<const std::string> GetStringArray() const {
+ std::span<const std::string> GetStringArray() const {
assert(m_val.type == NT_STRING_ARRAY);
- return m_string_array;
+ return *static_cast<std::vector<std::string>*>(m_storage.get());
}
/** @} */
@@ -232,9 +304,37 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeBoolean(bool value, uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_BOOLEAN, time, private_init());
- val->m_val.data.v_boolean = value;
+ static Value MakeBoolean(bool value, int64_t time = 0) {
+ Value val{NT_BOOLEAN, time, private_init{}};
+ val.m_val.data.v_boolean = value;
+ return val;
+ }
+
+ /**
+ * Creates an integer entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ */
+ static Value MakeInteger(int64_t value, int64_t time = 0) {
+ Value val{NT_INTEGER, time, private_init{}};
+ val.m_val.data.v_int = value;
+ return val;
+ }
+
+ /**
+ * Creates a float entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ */
+ static Value MakeFloat(float value, int64_t time = 0) {
+ Value val{NT_FLOAT, time, private_init{}};
+ val.m_val.data.v_float = value;
return val;
}
@@ -246,9 +346,9 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeDouble(double value, uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_DOUBLE, time, private_init());
- val->m_val.data.v_double = value;
+ static Value MakeDouble(double value, int64_t time = 0) {
+ Value val{NT_DOUBLE, time, private_init{}};
+ val.m_val.data.v_double = value;
return val;
}
@@ -260,12 +360,12 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeString(std::string_view value,
- uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_STRING, time, private_init());
- val->m_string = value;
- val->m_val.data.v_string.str = const_cast<char*>(val->m_string.c_str());
- val->m_val.data.v_string.len = val->m_string.size();
+ static Value MakeString(std::string_view value, int64_t time = 0) {
+ Value val{NT_STRING, time, private_init{}};
+ auto data = std::make_shared<std::string>(value);
+ val.m_val.data.v_string.str = const_cast<char*>(data->c_str());
+ val.m_val.data.v_string.len = data->size();
+ val.m_storage = std::move(data);
return val;
}
@@ -279,11 +379,12 @@
*/
template <typename T,
typename std::enable_if<std::is_same<T, std::string>::value>::type>
- static std::shared_ptr<Value> MakeString(T&& value, uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_STRING, time, private_init());
- val->m_string = std::forward<T>(value);
- val->m_val.data.v_string.str = const_cast<char*>(val->m_string.c_str());
- val->m_val.data.v_string.len = val->m_string.size();
+ static Value MakeString(T&& value, int64_t time = 0) {
+ Value val{NT_STRING, time, private_init{}};
+ auto data = std::make_shared<std::string>(std::forward(value));
+ val.m_val.data.v_string.str = const_cast<char*>(data->c_str());
+ val.m_val.data.v_string.len = data->size();
+ val.m_storage = std::move(data);
return val;
}
@@ -295,12 +396,13 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeRaw(std::string_view value,
- uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_RAW, time, private_init());
- val->m_string = value;
- val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
- val->m_val.data.v_raw.len = val->m_string.size();
+ static Value MakeRaw(std::span<const uint8_t> value, int64_t time = 0) {
+ Value val{NT_RAW, time, private_init{}};
+ auto data =
+ std::make_shared<std::vector<uint8_t>>(value.begin(), value.end());
+ val.m_val.data.v_raw.data = const_cast<uint8_t*>(data->data());
+ val.m_val.data.v_raw.size = data->size();
+ val.m_storage = std::move(data);
return val;
}
@@ -312,47 +414,14 @@
* time)
* @return The entry value
*/
- template <typename T,
- typename std::enable_if<std::is_same<T, std::string>::value>::type>
- static std::shared_ptr<Value> MakeRaw(T&& value, uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_RAW, time, private_init());
- val->m_string = std::forward<T>(value);
- val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
- val->m_val.data.v_raw.len = val->m_string.size();
- return val;
- }
-
- /**
- * Creates a rpc entry value.
- *
- * @param value the value
- * @param time if nonzero, the creation time to use (instead of the current
- * time)
- * @return The entry value
- */
- static std::shared_ptr<Value> MakeRpc(std::string_view value,
- uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_RPC, time, private_init());
- val->m_string = value;
- val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
- val->m_val.data.v_raw.len = val->m_string.size();
- return val;
- }
-
- /**
- * Creates a rpc entry value.
- *
- * @param value the value
- * @param time if nonzero, the creation time to use (instead of the current
- * time)
- * @return The entry value
- */
- template <typename T>
- static std::shared_ptr<Value> MakeRpc(T&& value, uint64_t time = 0) {
- auto val = std::make_shared<Value>(NT_RPC, time, private_init());
- val->m_string = std::forward<T>(value);
- val->m_val.data.v_raw.str = const_cast<char*>(val->m_string.c_str());
- val->m_val.data.v_raw.len = val->m_string.size();
+ template <typename T, typename std::enable_if<
+ std::is_same<T, std::vector<uint8_t>>::value>::type>
+ static Value MakeRaw(T&& value, int64_t time = 0) {
+ Value val{NT_RAW, time, private_init{}};
+ auto data = std::make_shared<std::vector<uint8_t>>(std::forward(value));
+ val.m_val.data.v_raw.data = const_cast<uint8_t*>(data->data());
+ val.m_val.data.v_raw.size = data->size();
+ val.m_storage = std::move(data);
return val;
}
@@ -364,8 +433,7 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeBooleanArray(wpi::span<const bool> value,
- uint64_t time = 0);
+ static Value MakeBooleanArray(std::span<const bool> value, int64_t time = 0);
/**
* Creates a boolean array entry value.
@@ -375,9 +443,9 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeBooleanArray(
- std::initializer_list<bool> value, uint64_t time = 0) {
- return MakeBooleanArray(wpi::span(value.begin(), value.end()), time);
+ static Value MakeBooleanArray(std::initializer_list<bool> value,
+ int64_t time = 0) {
+ return MakeBooleanArray(std::span(value.begin(), value.end()), time);
}
/**
@@ -388,8 +456,7 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeBooleanArray(wpi::span<const int> value,
- uint64_t time = 0);
+ static Value MakeBooleanArray(std::span<const int> value, int64_t time = 0);
/**
* Creates a boolean array entry value.
@@ -399,12 +466,95 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeBooleanArray(
- std::initializer_list<int> value, uint64_t time = 0) {
- return MakeBooleanArray(wpi::span(value.begin(), value.end()), time);
+ static Value MakeBooleanArray(std::initializer_list<int> value,
+ int64_t time = 0) {
+ return MakeBooleanArray(std::span(value.begin(), value.end()), time);
}
/**
+ * Creates a boolean array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ *
+ * @note This function moves the values out of the vector.
+ */
+ static Value MakeBooleanArray(std::vector<int>&& value, int64_t time = 0);
+
+ /**
+ * Creates an integer array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ */
+ static Value MakeIntegerArray(std::span<const int64_t> value,
+ int64_t time = 0);
+
+ /**
+ * Creates an integer array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ */
+ static Value MakeIntegerArray(std::initializer_list<int64_t> value,
+ int64_t time = 0) {
+ return MakeIntegerArray(std::span(value.begin(), value.end()), time);
+ }
+
+ /**
+ * Creates an integer array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ *
+ * @note This function moves the values out of the vector.
+ */
+ static Value MakeIntegerArray(std::vector<int64_t>&& value, int64_t time = 0);
+
+ /**
+ * Creates a float array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ */
+ static Value MakeFloatArray(std::span<const float> value, int64_t time = 0);
+
+ /**
+ * Creates a float array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ */
+ static Value MakeFloatArray(std::initializer_list<float> value,
+ int64_t time = 0) {
+ return MakeFloatArray(std::span(value.begin(), value.end()), time);
+ }
+
+ /**
+ * Creates a float array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ *
+ * @note This function moves the values out of the vector.
+ */
+ static Value MakeFloatArray(std::vector<float>&& value, int64_t time = 0);
+
+ /**
* Creates a double array entry value.
*
* @param value the value
@@ -412,8 +562,7 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeDoubleArray(wpi::span<const double> value,
- uint64_t time = 0);
+ static Value MakeDoubleArray(std::span<const double> value, int64_t time = 0);
/**
* Creates a double array entry value.
@@ -423,21 +572,22 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeDoubleArray(
- std::initializer_list<double> value, uint64_t time = 0) {
- return MakeDoubleArray(wpi::span(value.begin(), value.end()), time);
+ static Value MakeDoubleArray(std::initializer_list<double> value,
+ int64_t time = 0) {
+ return MakeDoubleArray(std::span(value.begin(), value.end()), time);
}
/**
- * Creates a string array entry value.
+ * Creates a double array entry value.
*
* @param value the value
* @param time if nonzero, the creation time to use (instead of the current
* time)
* @return The entry value
+ *
+ * @note This function moves the values out of the vector.
*/
- static std::shared_ptr<Value> MakeStringArray(
- wpi::span<const std::string> value, uint64_t time = 0);
+ static Value MakeDoubleArray(std::vector<double>&& value, int64_t time = 0);
/**
* Creates a string array entry value.
@@ -447,9 +597,20 @@
* time)
* @return The entry value
*/
- static std::shared_ptr<Value> MakeStringArray(
- std::initializer_list<std::string> value, uint64_t time = 0) {
- return MakeStringArray(wpi::span(value.begin(), value.end()), time);
+ static Value MakeStringArray(std::span<const std::string> value,
+ int64_t time = 0);
+
+ /**
+ * Creates a string array entry value.
+ *
+ * @param value the value
+ * @param time if nonzero, the creation time to use (instead of the current
+ * time)
+ * @return The entry value
+ */
+ static Value MakeStringArray(std::initializer_list<std::string> value,
+ int64_t time = 0) {
+ return MakeStringArray(std::span(value.begin(), value.end()), time);
}
/**
@@ -462,25 +623,19 @@
*
* @note This function moves the values out of the vector.
*/
- static std::shared_ptr<Value> MakeStringArray(
- std::vector<std::string>&& value, uint64_t time = 0);
+ static Value MakeStringArray(std::vector<std::string>&& value,
+ int64_t time = 0);
/** @} */
- Value(const Value&) = delete;
- Value& operator=(const Value&) = delete;
friend bool operator==(const Value& lhs, const Value& rhs);
private:
NT_Value m_val;
- std::string m_string;
- std::vector<std::string> m_string_array;
+ std::shared_ptr<void> m_storage;
};
bool operator==(const Value& lhs, const Value& rhs);
-inline bool operator!=(const Value& lhs, const Value& rhs) {
- return !(lhs == rhs);
-}
/**
* NetworkTable Value alias for similarity with Java.
@@ -489,5 +644,3 @@
using NetworkTableValue = Value;
} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_NETWORKTABLEVALUE_H_
diff --git a/ntcore/src/main/native/include/networktables/RpcCall.h b/ntcore/src/main/native/include/networktables/RpcCall.h
deleted file mode 100644
index 9c6e9f9..0000000
--- a/ntcore/src/main/native/include/networktables/RpcCall.h
+++ /dev/null
@@ -1,106 +0,0 @@
-// 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.
-
-#ifndef NTCORE_NETWORKTABLES_RPCCALL_H_
-#define NTCORE_NETWORKTABLES_RPCCALL_H_
-
-#include <string>
-#include <utility>
-
-#include "ntcore_c.h"
-
-namespace nt {
-
-class NetworkTableEntry;
-
-/**
- * NetworkTables Remote Procedure Call
- * @ingroup ntcore_cpp_api
- */
-class RpcCall final {
- public:
- /**
- * Construct invalid instance.
- */
- RpcCall() = default;
-
- /**
- * Construct from native handles.
- *
- * @param entry Entry handle
- * @param call Call handle
- */
- RpcCall(NT_Entry entry, NT_RpcCall call) : m_entry(entry), m_call(call) {}
-
- RpcCall(RpcCall&& other) noexcept;
- RpcCall(const RpcCall&) = delete;
- RpcCall& operator=(const RpcCall&) = delete;
-
- /**
- * Destructor. Cancels the result if no other action taken.
- */
- ~RpcCall();
-
- /**
- * Determines if the native handle is valid.
- *
- * @return True if the native handle is valid, false otherwise.
- */
- explicit operator bool() const { return m_call != 0; }
-
- /**
- * Get the RPC entry.
- *
- * @return NetworkTableEntry for the RPC.
- */
- NetworkTableEntry GetEntry() const;
-
- /**
- * Get the call native handle.
- *
- * @return Native handle.
- */
- NT_RpcCall GetCall() const { return m_call; }
-
- /**
- * Get the result (return value). This function blocks until
- * the result is received.
- *
- * @param result received result (output)
- * @return False on error, true otherwise.
- */
- bool GetResult(std::string* result);
-
- /**
- * Get the result (return value). This function blocks until
- * the result is received or it times out.
- *
- * @param result received result (output)
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return False on error or timeout, true otherwise.
- */
- bool GetResult(std::string* result, double timeout, bool* timed_out);
-
- /**
- * Ignore the result. This function is non-blocking.
- */
- void CancelResult();
-
- friend void swap(RpcCall& first, RpcCall& second) {
- using std::swap;
- swap(first.m_entry, second.m_entry);
- swap(first.m_call, second.m_call);
- }
-
- private:
- NT_Entry m_entry{0};
- NT_RpcCall m_call{0};
-};
-
-} // namespace nt
-
-#include "networktables/RpcCall.inc"
-
-#endif // NTCORE_NETWORKTABLES_RPCCALL_H_
diff --git a/ntcore/src/main/native/include/networktables/RpcCall.inc b/ntcore/src/main/native/include/networktables/RpcCall.inc
deleted file mode 100644
index 5e25b04..0000000
--- a/ntcore/src/main/native/include/networktables/RpcCall.inc
+++ /dev/null
@@ -1,51 +0,0 @@
-// 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.
-
-#ifndef NTCORE_NETWORKTABLES_RPCCALL_INC_
-#define NTCORE_NETWORKTABLES_RPCCALL_INC_
-
-#include <string>
-#include <utility>
-
-#include "networktables/RpcCall.h"
-#include "ntcore_cpp.h"
-
-namespace nt {
-
-inline RpcCall::RpcCall(RpcCall&& other) noexcept : RpcCall() {
- swap(*this, other);
-}
-
-inline RpcCall::~RpcCall() {
- // automatically cancel result if user didn't request it
- if (m_call != 0) {
- CancelResult();
- }
-}
-
-inline bool RpcCall::GetResult(std::string* result) {
- if (GetRpcResult(m_entry, m_call, result)) {
- m_call = 0;
- return true;
- }
- return false;
-}
-
-inline bool RpcCall::GetResult(std::string* result, double timeout,
- bool* timed_out) {
- if (GetRpcResult(m_entry, m_call, result, timeout, timed_out)) {
- m_call = 0;
- return true;
- }
- return false;
-}
-
-inline void RpcCall::CancelResult() {
- CancelRpcResult(m_entry, m_call);
- m_call = 0;
-}
-
-} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_RPCCALL_INC_
diff --git a/ntcore/src/main/native/include/networktables/TableEntryListener.h b/ntcore/src/main/native/include/networktables/TableEntryListener.h
deleted file mode 100644
index 180234f..0000000
--- a/ntcore/src/main/native/include/networktables/TableEntryListener.h
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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.
-
-#ifndef NTCORE_NETWORKTABLES_TABLEENTRYLISTENER_H_
-#define NTCORE_NETWORKTABLES_TABLEENTRYLISTENER_H_
-
-#include <functional>
-#include <memory>
-#include <string_view>
-
-namespace nt {
-
-class NetworkTable;
-class NetworkTableEntry;
-class Value;
-
-/**
- * A listener that listens to changes in values in a NetworkTable.
- *
- * Called when a key-value pair is changed in a NetworkTable.
- *
- * @param table the table the key-value pair exists in
- * @param key the key associated with the value that changed
- * @param entry the entry associated with the value that changed
- * @param value the new value
- * @param flags update flags; for example, EntryListenerFlags.kNew if the key
- * did not previously exist
- *
- * @ingroup ntcore_cpp_api
- */
-using TableEntryListener = std::function<void(
- NetworkTable* table, std::string_view name, NetworkTableEntry entry,
- std::shared_ptr<Value> value, int flags)>;
-
-} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_TABLEENTRYLISTENER_H_
diff --git a/ntcore/src/main/native/include/networktables/TableListener.h b/ntcore/src/main/native/include/networktables/TableListener.h
deleted file mode 100644
index cc1113e..0000000
--- a/ntcore/src/main/native/include/networktables/TableListener.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// 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.
-
-#ifndef NTCORE_NETWORKTABLES_TABLELISTENER_H_
-#define NTCORE_NETWORKTABLES_TABLELISTENER_H_
-
-#include <functional>
-#include <memory>
-#include <string_view>
-
-namespace nt {
-
-class NetworkTable;
-
-/**
- * A listener that listens to new sub-tables in a NetworkTable.
- *
- * Called when a new table is created.
- *
- * @param parent the parent of the table
- * @param name the name of the new table
- * @param table the new table
- *
- * @ingroup ntcore_cpp_api
- */
-using TableListener =
- std::function<void(NetworkTable* parent, std::string_view name,
- std::shared_ptr<NetworkTable> table)>;
-
-} // namespace nt
-
-#endif // NTCORE_NETWORKTABLES_TABLELISTENER_H_
diff --git a/ntcore/src/main/native/include/networktables/Topic.h b/ntcore/src/main/native/include/networktables/Topic.h
new file mode 100644
index 0000000..e2a8a5a
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/Topic.h
@@ -0,0 +1,385 @@
+// 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 <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "networktables/NetworkTableType.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class json;
+} // namespace wpi
+
+namespace nt {
+
+class GenericEntry;
+class GenericPublisher;
+class GenericSubscriber;
+class NetworkTableInstance;
+
+/** NetworkTables Topic. */
+class Topic {
+ public:
+ Topic() = default;
+ explicit Topic(NT_Topic handle) : m_handle{handle} {}
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ explicit operator bool() const { return m_handle != 0; }
+
+ /**
+ * Gets the native handle for the topic.
+ *
+ * @return Native handle
+ */
+ NT_Topic GetHandle() const { return m_handle; }
+
+ /**
+ * Gets the instance for the topic.
+ *
+ * @return Instance
+ */
+ NetworkTableInstance GetInstance() const;
+
+ /**
+ * Gets the name of the topic.
+ *
+ * @return the topic's name
+ */
+ std::string GetName() const;
+
+ /**
+ * Gets the type of the topic.
+ *
+ * @return the topic's type
+ */
+ NetworkTableType GetType() const;
+
+ /**
+ * Gets the type string of the topic. This may have more information
+ * than the numeric type (especially for raw values).
+ *
+ * @return the topic's type
+ */
+ std::string GetTypeString() const;
+
+ /**
+ * Make value persistent through server restarts.
+ *
+ * @param persistent True for persistent, false for not persistent.
+ */
+ void SetPersistent(bool persistent);
+
+ /**
+ * Returns whether the value is persistent through server restarts.
+ *
+ * @return True if the value is persistent.
+ */
+ bool IsPersistent() const;
+
+ /**
+ * Make the server retain the topic even when there are no publishers.
+ *
+ * @param retained True for retained, false for not retained.
+ */
+ void SetRetained(bool retained);
+
+ /**
+ * Returns whether the topic is retained by server when there are no
+ * publishers.
+ *
+ * @return True if the topic is retained.
+ */
+ bool IsRetained() const;
+
+ /**
+ * Determines if the topic is currently being published.
+ *
+ * @return True if the topic exists, false otherwise.
+ */
+ bool Exists() const;
+
+ /**
+ * Gets the current value of a property (as a JSON object).
+ *
+ * @param name property name
+ * @return JSON object; null object if the property does not exist.
+ */
+ wpi::json GetProperty(std::string_view name) const;
+
+ /**
+ * Sets a property value.
+ *
+ * @param name property name
+ * @param value property value
+ */
+ void SetProperty(std::string_view name, const wpi::json& value);
+
+ /**
+ * Deletes a property. Has no effect if the property does not exist.
+ *
+ * @param name property name
+ */
+ void DeleteProperty(std::string_view name);
+
+ /**
+ * Gets all topic properties as a JSON object. Each key in the object
+ * is the property name, and the corresponding value is the property value.
+ *
+ * @return JSON object
+ */
+ wpi::json GetProperties() const;
+
+ /**
+ * Updates multiple topic properties. Each key in the passed-in object is
+ * the name of the property to add/update, and the corresponding value is the
+ * property value to set for that property. Null values result in deletion
+ * of the corresponding property.
+ *
+ * @param properties JSON object with keys to add/update/delete
+ * @return False if properties is not an object
+ */
+ bool SetProperties(const wpi::json& properties);
+
+ /**
+ * Gets combined information about the topic.
+ *
+ * @return Topic information
+ */
+ TopicInfo GetInfo() const;
+
+ /**
+ * Create a new subscriber to the topic.
+ *
+ * <p>The subscriber is only active as long as the returned object
+ * is not destroyed.
+ *
+ * @param options subscribe options
+ * @return subscriber
+ */
+ [[nodiscard]] GenericSubscriber GenericSubscribe(
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * 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 typeString type string
+ * @param options subscribe options
+ * @return subscriber
+ */
+ [[nodiscard]] GenericSubscriber GenericSubscribe(
+ std::string_view typeString,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * 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 typeString type string
+ * @param options publish options
+ * @return publisher
+ */
+ [[nodiscard]] GenericPublisher GenericPublish(
+ std::string_view typeString,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * 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 typeString type string
+ * @param properties JSON properties
+ * @param options publish options
+ * @return publisher
+ */
+ [[nodiscard]] GenericPublisher GenericPublishEx(
+ std::string_view typeString, const wpi::json& properties,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * Create a new generic 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 options publish and/or subscribe options
+ * @return entry
+ */
+ [[nodiscard]] GenericEntry GetGenericEntry(
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * Create a new generic 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 typeString type string
+ * @param options publish and/or subscribe options
+ * @return entry
+ */
+ [[nodiscard]] GenericEntry GetGenericEntry(
+ std::string_view typeString,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * Equality operator. Returns true if both instances refer to the same
+ * native handle.
+ */
+ bool operator==(const Topic&) const = default;
+
+ protected:
+ NT_Topic m_handle{0};
+};
+
+/** NetworkTables subscriber. */
+class Subscriber {
+ public:
+ virtual ~Subscriber();
+
+ Subscriber(const Subscriber&) = delete;
+ Subscriber& operator=(const Subscriber&) = delete;
+
+ Subscriber(Subscriber&&);
+ Subscriber& operator=(Subscriber&&);
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ explicit operator bool() const { return m_subHandle != 0; }
+
+ /**
+ * Gets the native handle for the subscriber.
+ *
+ * @return Native handle
+ */
+ NT_Subscriber GetHandle() const { return m_subHandle; }
+
+ /**
+ * Determines if the topic is currently being published.
+ *
+ * @return True if the topic exists, false otherwise.
+ */
+ bool Exists() const;
+
+ /**
+ * Gets the last time the value was changed.
+ * Note: this is not atomic with Get(); use GetAtomic() to get
+ * both the value and last change as an atomic operation.
+ *
+ * @return Topic last change time
+ */
+ int64_t GetLastChange() const;
+
+ /**
+ * Gets the subscribed-to topic.
+ *
+ * @return Topic
+ */
+ Topic GetTopic() const;
+
+ protected:
+ Subscriber() = default;
+ explicit Subscriber(NT_Subscriber handle) : m_subHandle{handle} {}
+
+ NT_Subscriber m_subHandle{0};
+};
+
+/** NetworkTables publisher. */
+class Publisher {
+ public:
+ virtual ~Publisher();
+
+ Publisher(const Publisher&) = delete;
+ Publisher& operator=(const Publisher&) = delete;
+
+ Publisher(Publisher&&);
+ Publisher& operator=(Publisher&&);
+
+ /**
+ * Determines if the native handle is valid.
+ *
+ * @return True if the native handle is valid, false otherwise.
+ */
+ explicit operator bool() const { return m_pubHandle != 0; }
+
+ /**
+ * Gets the native handle for the publisher.
+ *
+ * @return Native handle
+ */
+ NT_Publisher GetHandle() const { return m_pubHandle; }
+
+ /**
+ * Gets the published-to topic.
+ *
+ * @return Topic
+ */
+ Topic GetTopic() const;
+
+ protected:
+ Publisher() = default;
+ explicit Publisher(NT_Publisher handle) : m_pubHandle{handle} {}
+
+ NT_Publisher m_pubHandle{0};
+};
+
+} // namespace nt
+
+#include "networktables/Topic.inc"
diff --git a/ntcore/src/main/native/include/networktables/Topic.inc b/ntcore/src/main/native/include/networktables/Topic.inc
new file mode 100644
index 0000000..642e49e
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/Topic.inc
@@ -0,0 +1,115 @@
+// 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 <string>
+
+#include "networktables/NetworkTableInstance.h"
+#include "networktables/NetworkTableType.h"
+#include "networktables/Topic.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+inline NetworkTableInstance Topic::GetInstance() const {
+ return NetworkTableInstance{GetInstanceFromHandle(m_handle)};
+}
+
+inline std::string Topic::GetName() const {
+ return ::nt::GetTopicName(m_handle);
+}
+
+inline NetworkTableType Topic::GetType() const {
+ return static_cast<NetworkTableType>(::nt::GetTopicType(m_handle));
+}
+
+inline std::string Topic::GetTypeString() const {
+ return ::nt::GetTopicTypeString(m_handle);
+}
+
+inline void Topic::SetPersistent(bool persistent) {
+ ::nt::SetTopicPersistent(m_handle, persistent);
+}
+
+inline bool Topic::IsPersistent() const {
+ return ::nt::GetTopicPersistent(m_handle);
+}
+
+inline void Topic::SetRetained(bool retained) {
+ ::nt::SetTopicRetained(m_handle, retained);
+}
+
+inline bool Topic::IsRetained() const {
+ return ::nt::GetTopicRetained(m_handle);
+}
+
+inline bool Topic::Exists() const {
+ return nt::GetTopicExists(m_handle);
+}
+
+inline void Topic::DeleteProperty(std::string_view name) {
+ ::nt::DeleteTopicProperty(m_handle, name);
+}
+
+inline bool Topic::SetProperties(const wpi::json& properties) {
+ return ::nt::SetTopicProperties(m_handle, properties);
+}
+
+inline TopicInfo Topic::GetInfo() const {
+ return ::nt::GetTopicInfo(m_handle);
+}
+
+inline Subscriber::~Subscriber() {
+ ::nt::Release(m_subHandle);
+}
+
+inline Subscriber::Subscriber(Subscriber&& rhs) : m_subHandle{rhs.m_subHandle} {
+ rhs.m_subHandle = 0;
+}
+
+inline Subscriber& Subscriber::operator=(Subscriber&& rhs) {
+ if (m_subHandle != 0) {
+ ::nt::Release(m_subHandle);
+ }
+ m_subHandle = rhs.m_subHandle;
+ rhs.m_subHandle = 0;
+ return *this;
+}
+
+inline bool Subscriber::Exists() const {
+ return nt::GetTopicExists(m_subHandle);
+}
+
+inline int64_t Subscriber::GetLastChange() const {
+ return ::nt::GetEntryLastChange(m_subHandle);
+}
+
+inline Topic Subscriber::GetTopic() const {
+ return Topic{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+inline Publisher::~Publisher() {
+ ::nt::Release(m_pubHandle);
+}
+
+inline Publisher::Publisher(Publisher&& rhs) : m_pubHandle{rhs.m_pubHandle} {
+ rhs.m_pubHandle = 0;
+}
+
+inline Publisher& Publisher::operator=(Publisher&& rhs) {
+ if (m_pubHandle != 0) {
+ ::nt::Release(m_pubHandle);
+ }
+ m_pubHandle = rhs.m_pubHandle;
+ rhs.m_pubHandle = 0;
+ return *this;
+}
+
+inline Topic Publisher::GetTopic() const {
+ return Topic{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+} // namespace nt
diff --git a/ntcore/src/main/native/include/networktables/UnitTopic.h b/ntcore/src/main/native/include/networktables/UnitTopic.h
new file mode 100644
index 0000000..cac9501
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/UnitTopic.h
@@ -0,0 +1,414 @@
+// 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 <span>
+#include <string_view>
+#include <vector>
+
+#include "networktables/Topic.h"
+#include "ntcore_cpp.h"
+
+namespace wpi {
+class json;
+} // namespace wpi
+
+namespace nt {
+
+template <typename T>
+class UnitTopic;
+
+/**
+ * Timestamped unit.
+ *
+ * @tparam T unit type, e.g. units::meter_t
+ */
+template <typename T>
+struct TimestampedUnit {
+ TimestampedUnit() = default;
+ TimestampedUnit(int64_t time, int64_t serverTime, T value)
+ : time{time}, serverTime{serverTime}, value{value} {}
+
+ /**
+ * Time in local time base.
+ */
+ int64_t time = 0;
+
+ /**
+ * Time in server time base. May be 0 or 1 for locally set values.
+ */
+ int64_t serverTime = 0;
+
+ /**
+ * Value.
+ */
+ T value = {};
+};
+
+/**
+ * NetworkTables unit-typed subscriber.
+ *
+ * @tparam T unit type, e.g. units::meter_t
+ */
+template <typename T>
+class UnitSubscriber : public Subscriber {
+ public:
+ using TopicType = UnitTopic<T>;
+ using ValueType = T;
+ using ParamType = T;
+ using TimestampedValueType = TimestampedUnit<T>;
+
+ UnitSubscriber() = default;
+
+ /**
+ * Construct from a subscriber handle; recommended to use
+ * UnitTopic::Subscribe() instead.
+ *
+ * @param handle Native handle
+ * @param defaultValue Default value
+ */
+ UnitSubscriber(NT_Subscriber handle, ParamType defaultValue);
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the stored default value.
+ *
+ * @return value
+ */
+ ValueType Get() const;
+
+ /**
+ * Get the last published value.
+ * If no value has been published, returns the passed defaultValue.
+ *
+ * @param defaultValue default value to return if no value has been published
+ * @return value
+ */
+ ValueType Get(ParamType defaultValue) const;
+
+ /**
+ * Get the last published value along with its timestamp
+ * If no value has been published, returns the stored default value and a
+ * timestamp of 0.
+ *
+ * @return timestamped value
+ */
+ TimestampedValueType GetAtomic() const;
+
+ /**
+ * Get the last published value along with its timestamp.
+ * If no value has been published, 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(ParamType defaultValue) const;
+
+ /**
+ * Get an array of all value changes since the last call to ReadQueue.
+ * Also provides a timestamp for each value.
+ *
+ * @note The "poll storage" subscribe option can be used to set the queue
+ * depth.
+ *
+ * @return Array of timestamped values; empty array if no new changes have
+ * been published since the previous call.
+ */
+ std::vector<TimestampedValueType> ReadQueue();
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+
+ private:
+ ValueType m_defaultValue;
+};
+
+/**
+ * NetworkTables unit-typed publisher.
+ *
+ * @tparam T unit type, e.g. units::meter_t
+ */
+template <typename T>
+class UnitPublisher : public Publisher {
+ public:
+ using TopicType = UnitTopic<T>;
+ using ValueType = T;
+ using ParamType = T;
+
+ using TimestampedValueType = TimestampedUnit<T>;
+
+ UnitPublisher() = default;
+
+ /**
+ * Construct from a publisher handle; recommended to use
+ * UnitTopic::Publish() instead.
+ *
+ * @param handle Native handle
+ */
+ explicit UnitPublisher(NT_Publisher handle);
+
+ /**
+ * Publish a new value.
+ *
+ * @param value value to publish
+ * @param time timestamp; 0 indicates current NT time should be used
+ */
+ void Set(ParamType value, int64_t time = 0);
+
+ /**
+ * Publish a default value.
+ * On reconnect, a default value will never be used in preference to a
+ * published value.
+ *
+ * @param value value
+ */
+ void SetDefault(ParamType value);
+
+ /**
+ * Get the corresponding topic.
+ *
+ * @return Topic
+ */
+ TopicType GetTopic() const;
+};
+
+/**
+ * NetworkTables unit-typed entry.
+ *
+ * @note Unlike NetworkTableEntry, the entry goes away when this is destroyed.
+ *
+ * @tparam T unit type, e.g. units::meter_t
+ */
+template <typename T>
+class UnitEntry final : public UnitSubscriber<T>, public UnitPublisher<T> {
+ public:
+ using SubscriberType = UnitSubscriber<T>;
+ using PublisherType = UnitPublisher<T>;
+ using TopicType = UnitTopic<T>;
+ using ValueType = T;
+ using ParamType = T;
+
+ using TimestampedValueType = TimestampedUnit<T>;
+
+ UnitEntry() = default;
+
+ /**
+ * Construct from an entry handle; recommended to use
+ * UnitTopic::GetEntry() instead.
+ *
+ * @param handle Native handle
+ * @param defaultValue Default value
+ */
+ UnitEntry(NT_Entry handle, ParamType defaultValue);
+
+ /**
+ * 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;
+
+ /**
+ * Stops publishing the entry if it's published.
+ */
+ void Unpublish();
+};
+
+/**
+ * NetworkTables unit-typed topic. Publishers publish the type name (e.g.
+ * "meter") as the "unit" property. Type conversions are not performed--for
+ * correct behavior the publisher and subscriber must use the same unit type,
+ * but this can be checked at runtime using IsMatchingUnit().
+ *
+ * @tparam T unit type, e.g. units::meter_t
+ */
+template <typename T>
+class UnitTopic final : public Topic {
+ public:
+ using SubscriberType = UnitSubscriber<T>;
+ using PublisherType = UnitPublisher<T>;
+ using EntryType = UnitEntry<T>;
+ using ValueType = T;
+ using ParamType = T;
+ using TimestampedValueType = TimestampedUnit<T>;
+ /** The default type string for this topic type. */
+ static constexpr std::string_view kTypeString = "double";
+
+ UnitTopic() = default;
+
+ /**
+ * Construct from a topic handle.
+ *
+ * @param handle Native handle
+ */
+ explicit UnitTopic(NT_Topic handle) : Topic{handle} {}
+
+ /**
+ * Construct from a generic topic.
+ *
+ * @param topic Topic
+ */
+ explicit UnitTopic(Topic topic) : Topic{topic} {}
+
+ /**
+ * Verify the topic has a matching unit type (if the topic is published).
+ *
+ * @return True if unit matches, false if not matching or topic not published.
+ */
+ bool IsMatchingUnit() const;
+
+ /**
+ * 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(
+ ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * Create a new subscriber to the topic, with specific type string.
+ *
+ * <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 typeString type string
+ * @param defaultValue default value used when a default is not provided to a
+ * getter function
+ * @param options subscribe options
+ * @return subscriber
+ */
+ [[nodiscard]] SubscriberType SubscribeEx(
+ std::string_view typeString, ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * 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);
+
+ /**
+ * 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 typeString type string
+ * @param properties JSON properties
+ * @param options publish options
+ * @return publisher
+ */
+ [[nodiscard]] PublisherType PublishEx(
+ std::string_view typeString, const wpi::json& properties,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * 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(
+ ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+ /**
+ * Create a new entry for the topic, with specific type string.
+ *
+ * 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 typeString type string
+ * @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 GetEntryEx(
+ std::string_view typeString, ParamType defaultValue,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+};
+
+} // namespace nt
+
+#include "networktables/UnitTopic.inc"
diff --git a/ntcore/src/main/native/include/networktables/UnitTopic.inc b/ntcore/src/main/native/include/networktables/UnitTopic.inc
new file mode 100644
index 0000000..c107526
--- /dev/null
+++ b/ntcore/src/main/native/include/networktables/UnitTopic.inc
@@ -0,0 +1,140 @@
+// 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 <vector>
+
+#include <wpi/json.h>
+
+#include "networktables/NetworkTableType.h"
+#include "networktables/UnitTopic.h"
+#include "ntcore_cpp.h"
+
+namespace nt {
+
+template <typename T>
+inline UnitSubscriber<T>::UnitSubscriber(NT_Subscriber handle, T defaultValue)
+ : Subscriber{handle}, m_defaultValue{defaultValue} {}
+
+template <typename T>
+inline T UnitSubscriber<T>::Get() const {
+ return Get(m_defaultValue);
+}
+
+template <typename T>
+inline T UnitSubscriber<T>::Get(T defaultValue) const {
+ return T{::nt::GetDouble(m_subHandle, defaultValue.value())};
+}
+
+template <typename T>
+inline TimestampedUnit<T> UnitSubscriber<T>::GetAtomic() const {
+ return GetAtomic(m_defaultValue);
+}
+
+template <typename T>
+inline TimestampedUnit<T> UnitSubscriber<T>::GetAtomic(T defaultValue) const {
+ auto doubleVal = ::nt::GetAtomicDouble(m_subHandle, defaultValue.value());
+ return {doubleVal.time, doubleVal.serverTime, doubleVal.value};
+}
+
+template <typename T>
+inline std::vector<TimestampedUnit<T>> UnitSubscriber<T>::ReadQueue() {
+ std::vector<TimestampedUnit<T>> vals;
+ auto doubleVals = ::nt::ReadQueueDouble(m_subHandle);
+ vals.reserve(doubleVals.size());
+ for (auto&& val : doubleVals) {
+ vals.emplace_back(val.time, val.serverTime, val.value);
+ }
+}
+
+template <typename T>
+inline UnitTopic<T> UnitSubscriber<T>::GetTopic() const {
+ return UnitTopic<T>{::nt::GetTopicFromHandle(m_subHandle)};
+}
+
+template <typename T>
+inline UnitPublisher<T>::UnitPublisher(NT_Publisher handle)
+ : Publisher{handle} {}
+
+template <typename T>
+inline void UnitPublisher<T>::Set(T value, int64_t time) {
+ ::nt::SetDouble(m_pubHandle, value.value(), time);
+}
+
+template <typename T>
+inline void UnitPublisher<T>::SetDefault(T value) {
+ ::nt::SetDefaultDouble(m_pubHandle, value.value());
+}
+
+template <typename T>
+inline UnitTopic<T> UnitPublisher<T>::GetTopic() const {
+ return UnitTopic<T>{::nt::GetTopicFromHandle(m_pubHandle)};
+}
+
+template <typename T>
+inline UnitEntry<T>::UnitEntry(NT_Entry handle, T defaultValue)
+ : UnitSubscriber<T>{handle, defaultValue}, UnitPublisher<T>{handle} {}
+
+template <typename T>
+inline UnitTopic<T> UnitEntry<T>::GetTopic() const {
+ return UnitTopic<T>{::nt::GetTopicFromHandle(this->m_subHandle)};
+}
+
+template <typename T>
+inline void UnitEntry<T>::Unpublish() {
+ ::nt::Unpublish(this->m_pubHandle);
+}
+
+template <typename T>
+inline bool UnitTopic<T>::IsMatchingUnit() const {
+ return GetProperty("unit") == T{}.name();
+}
+
+template <typename T>
+inline UnitSubscriber<T> UnitTopic<T>::Subscribe(T defaultValue,
+ const PubSubOptions& options) {
+ return UnitSubscriber<T>{
+ ::nt::Subscribe(m_handle, NT_DOUBLE, "double", options), defaultValue};
+}
+
+template <typename T>
+inline UnitSubscriber<T> UnitTopic<T>::SubscribeEx(
+ std::string_view typeString, T defaultValue, const PubSubOptions& options) {
+ return UnitSubscriber<T>{
+ ::nt::Subscribe(m_handle, NT_DOUBLE, typeString, options), defaultValue};
+}
+
+template <typename T>
+inline UnitPublisher<T> UnitTopic<T>::Publish(const PubSubOptions& options) {
+ return UnitPublisher<T>{::nt::PublishEx(m_handle, NT_DOUBLE, "double",
+ {{"unit", T{}.name()}}, options)};
+}
+
+template <typename T>
+inline UnitPublisher<T> UnitTopic<T>::PublishEx(std::string_view typeString,
+ const wpi::json& properties,
+ const PubSubOptions& options) {
+ wpi::json props = properties;
+ props["unit"] = T{}.name();
+ return UnitPublisher<T>{
+ ::nt::PublishEx(m_handle, NT_DOUBLE, typeString, props, options)};
+}
+
+template <typename T>
+inline UnitEntry<T> UnitTopic<T>::GetEntry(T defaultValue,
+ const PubSubOptions& options) {
+ return UnitEntry<T>{::nt::GetEntry(m_handle, NT_DOUBLE, "double", options),
+ defaultValue};
+}
+
+template <typename T>
+inline UnitEntry<T> UnitTopic<T>::GetEntryEx(std::string_view typeString,
+ T defaultValue,
+ const PubSubOptions& options) {
+ return UnitEntry<T>{::nt::GetEntry(m_handle, NT_DOUBLE, typeString, options),
+ defaultValue};
+}
+
+} // namespace nt
diff --git a/ntcore/src/main/native/include/ntcore.h b/ntcore/src/main/native/include/ntcore.h
index 5cdd473..e4ae7f9 100644
--- a/ntcore/src/main/native/include/ntcore.h
+++ b/ntcore/src/main/native/include/ntcore.h
@@ -2,8 +2,7 @@
// 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.
-#ifndef NTCORE_NTCORE_H_
-#define NTCORE_NTCORE_H_
+#pragma once
/* C API */
#include "ntcore_c.h"
@@ -12,5 +11,3 @@
/* C++ API */
#include "ntcore_cpp.h"
#endif /* __cplusplus */
-
-#endif // NTCORE_NTCORE_H_
diff --git a/ntcore/src/main/native/include/ntcore_c.h b/ntcore/src/main/native/include/ntcore_c.h
index 3ee6d51..e9e9f81 100644
--- a/ntcore/src/main/native/include/ntcore_c.h
+++ b/ntcore/src/main/native/include/ntcore_c.h
@@ -2,8 +2,7 @@
// 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.
-#ifndef NTCORE_NTCORE_C_H_
-#define NTCORE_NTCORE_C_H_
+#pragma once
#include <stdint.h>
@@ -29,19 +28,22 @@
typedef int NT_Bool;
typedef unsigned int NT_Handle;
-typedef NT_Handle NT_ConnectionListener;
-typedef NT_Handle NT_ConnectionListenerPoller;
+typedef NT_Handle NT_ConnectionDataLogger;
+typedef NT_Handle NT_DataLogger;
typedef NT_Handle NT_Entry;
-typedef NT_Handle NT_EntryListener;
-typedef NT_Handle NT_EntryListenerPoller;
typedef NT_Handle NT_Inst;
-typedef NT_Handle NT_Logger;
-typedef NT_Handle NT_LoggerPoller;
-typedef NT_Handle NT_RpcCall;
-typedef NT_Handle NT_RpcCallPoller;
+typedef NT_Handle NT_Listener;
+typedef NT_Handle NT_ListenerPoller;
+typedef NT_Handle NT_MultiSubscriber;
+typedef NT_Handle NT_Topic;
+typedef NT_Handle NT_Subscriber;
+typedef NT_Handle NT_Publisher;
-/** Default network tables port number */
-#define NT_DEFAULT_PORT 1735
+/** Default network tables port number (NT3) */
+#define NT_DEFAULT_PORT3 1735
+
+/** Default network tables port number (NT4) */
+#define NT_DEFAULT_PORT4 5810
/** NetworkTables data types. */
enum NT_Type {
@@ -53,11 +55,15 @@
NT_BOOLEAN_ARRAY = 0x10,
NT_DOUBLE_ARRAY = 0x20,
NT_STRING_ARRAY = 0x40,
- NT_RPC = 0x80
+ NT_RPC = 0x80,
+ NT_INTEGER = 0x100,
+ NT_FLOAT = 0x200,
+ NT_INTEGER_ARRAY = 0x400,
+ NT_FLOAT_ARRAY = 0x800
};
/** NetworkTables entry flags. */
-enum NT_EntryFlags { NT_PERSISTENT = 0x01 };
+enum NT_EntryFlags { NT_PERSISTENT = 0x01, NT_RETAINED = 0x02 };
/** NetworkTables logging levels. */
enum NT_LogLevel {
@@ -72,27 +78,47 @@
NT_LOG_DEBUG4 = 6
};
-/** NetworkTables notifier kinds. */
-enum NT_NotifyKind {
- NT_NOTIFY_NONE = 0,
- NT_NOTIFY_IMMEDIATE = 0x01, /* initial listener addition */
- NT_NOTIFY_LOCAL = 0x02, /* changed locally */
- NT_NOTIFY_NEW = 0x04, /* newly created entry */
- NT_NOTIFY_DELETE = 0x08, /* deleted */
- NT_NOTIFY_UPDATE = 0x10, /* value changed */
- NT_NOTIFY_FLAGS = 0x20 /* flags changed */
-};
-
/** Client/server modes */
enum NT_NetworkMode {
NT_NET_MODE_NONE = 0x00, /* not running */
NT_NET_MODE_SERVER = 0x01, /* running in server mode */
- NT_NET_MODE_CLIENT = 0x02, /* running in client mode */
- NT_NET_MODE_STARTING = 0x04, /* flag for starting (either client or server) */
- NT_NET_MODE_FAILURE = 0x08, /* flag for failure (either client or server) */
+ NT_NET_MODE_CLIENT3 = 0x02, /* running in NT3 client mode */
+ NT_NET_MODE_CLIENT4 = 0x04, /* running in NT4 client mode */
+ NT_NET_MODE_STARTING = 0x08, /* flag for starting (either client or server) */
NT_NET_MODE_LOCAL = 0x10, /* running in local-only mode */
};
+/** Event notification flags. */
+enum NT_EventFlags {
+ NT_EVENT_NONE = 0,
+ /** Initial listener addition. */
+ NT_EVENT_IMMEDIATE = 0x01,
+ /** Client connected (on server, any client connected). */
+ NT_EVENT_CONNECTED = 0x02,
+ /** Client disconnected (on server, any client disconnected). */
+ NT_EVENT_DISCONNECTED = 0x04,
+ /** Any connection event (connect or disconnect). */
+ NT_EVENT_CONNECTION = NT_EVENT_CONNECTED | NT_EVENT_DISCONNECTED,
+ /** New topic published. */
+ NT_EVENT_PUBLISH = 0x08,
+ /** Topic unpublished. */
+ NT_EVENT_UNPUBLISH = 0x10,
+ /** Topic properties changed. */
+ NT_EVENT_PROPERTIES = 0x20,
+ /** Any topic event (publish, unpublish, or properties changed). */
+ NT_EVENT_TOPIC = NT_EVENT_PUBLISH | NT_EVENT_UNPUBLISH | NT_EVENT_PROPERTIES,
+ /** Topic value updated (via network). */
+ NT_EVENT_VALUE_REMOTE = 0x40,
+ /** Topic value updated (local). */
+ NT_EVENT_VALUE_LOCAL = 0x80,
+ /** Topic value updated (network or local). */
+ NT_EVENT_VALUE_ALL = NT_EVENT_VALUE_REMOTE | NT_EVENT_VALUE_LOCAL,
+ /** Log message. */
+ NT_EVENT_LOGMESSAGE = 0x100,
+ /** Time synchronized with server. */
+ NT_EVENT_TIMESYNC = 0x200,
+};
+
/*
* Structures
*/
@@ -117,12 +143,18 @@
/** NetworkTables Entry Value. Note this is a typed union. */
struct NT_Value {
enum NT_Type type;
- uint64_t last_change;
+ int64_t last_change;
+ int64_t server_time;
union {
NT_Bool v_boolean;
+ int64_t v_int;
+ float v_float;
double v_double;
struct NT_String v_string;
- struct NT_String v_raw;
+ struct {
+ uint8_t* data;
+ size_t size;
+ } v_raw;
struct {
NT_Bool* arr;
size_t size;
@@ -132,35 +164,42 @@
size_t size;
} arr_double;
struct {
+ float* arr;
+ size_t size;
+ } arr_float;
+ struct {
+ int64_t* arr;
+ size_t size;
+ } arr_int;
+ struct {
struct NT_String* arr;
size_t size;
} arr_string;
} data;
};
-/** NetworkTables Entry Information */
-struct NT_EntryInfo {
- /** Entry handle */
- NT_Entry entry;
+/** NetworkTables Topic Information */
+struct NT_TopicInfo {
+ /** Topic handle */
+ NT_Topic topic;
- /** Entry name */
+ /** Topic name */
struct NT_String name;
- /** Entry type */
+ /** Topic type */
enum NT_Type type;
- /** Entry flags */
- unsigned int flags;
+ /** Topic type string */
+ struct NT_String type_str;
- /** Timestamp of last change to entry (type or value). */
- uint64_t last_change;
+ /** Topic properties JSON string */
+ struct NT_String properties;
};
/** NetworkTables Connection Information */
struct NT_ConnectionInfo {
/**
- * The remote identifier (as set on the remote node by
- * NetworkTableInstance::SetNetworkIdentity() or nt::SetNetworkIdentity()).
+ * The remote identifier (as set on the remote node by NT_StartClient4().
*/
struct NT_String remote_id;
@@ -183,75 +222,20 @@
unsigned int protocol_version;
};
-/** NetworkTables RPC Version 1 Definition Parameter */
-struct NT_RpcParamDef {
- struct NT_String name;
- struct NT_Value def_value;
-};
+/** NetworkTables value event data. */
+struct NT_ValueEventData {
+ /** Topic handle. */
+ NT_Topic topic;
-/** NetworkTables RPC Version 1 Definition Result */
-struct NT_RpcResultDef {
- struct NT_String name;
- enum NT_Type type;
-};
-
-/** NetworkTables RPC Version 1 Definition */
-struct NT_RpcDefinition {
- unsigned int version;
- struct NT_String name;
- size_t num_params;
- struct NT_RpcParamDef* params;
- size_t num_results;
- struct NT_RpcResultDef* results;
-};
-
-/** NetworkTables RPC Call Data */
-struct NT_RpcAnswer {
- NT_Entry entry;
- NT_RpcCall call;
- struct NT_String name;
- struct NT_String params;
- struct NT_ConnectionInfo conn;
-};
-
-/** NetworkTables Entry Notification */
-struct NT_EntryNotification {
- /** Listener that was triggered. */
- NT_EntryListener listener;
-
- /** Entry handle. */
- NT_Entry entry;
-
- /** Entry name. */
- struct NT_String name;
+ /** Subscriber/entry handle. */
+ NT_Handle subentry;
/** The new value. */
struct NT_Value value;
-
- /**
- * Update flags. For example, NT_NOTIFY_NEW if the key did not previously
- * exist.
- */
- unsigned int flags;
-};
-
-/** NetworkTables Connection Notification */
-struct NT_ConnectionNotification {
- /** Listener that was triggered. */
- NT_ConnectionListener listener;
-
- /** True if event is due to connection being established. */
- NT_Bool connected;
-
- /** Connection info. */
- struct NT_ConnectionInfo conn;
};
/** NetworkTables log message. */
struct NT_LogMessage {
- /** The logger that generated the message. */
- NT_Logger logger;
-
/** Log level of the message. See NT_LogLevel. */
unsigned int level;
@@ -265,6 +249,120 @@
char* message;
};
+/** NetworkTables time sync event data. */
+struct NT_TimeSyncEventData {
+ /**
+ * Offset between local time and server time, in microseconds. Add this value
+ * to local time to get the estimated equivalent server time.
+ */
+ int64_t serverTimeOffset;
+
+ /** Measured round trip time divided by 2, in microseconds. */
+ int64_t rtt2;
+
+ /**
+ * If serverTimeOffset and RTT are valid. An event with this set to false is
+ * sent when the client disconnects.
+ */
+ NT_Bool valid;
+};
+
+/** NetworkTables event */
+struct NT_Event {
+ /** Listener that triggered this event. */
+ NT_Handle listener;
+
+ /**
+ * Event flags (NT_EventFlags). Also indicates the data included with the
+ * event:
+ * - NT_EVENT_CONNECTED or NT_EVENT_DISCONNECTED: connInfo
+ * - NT_EVENT_PUBLISH, NT_EVENT_UNPUBLISH, or NT_EVENT_PROPERTIES: topicInfo
+ * - NT_EVENT_VALUE_REMOTE, NT_NOTIFY_VALUE_LOCAL: valueData
+ * - NT_EVENT_LOGMESSAGE: logMessage
+ * - NT_EVENT_TIMESYNC: timeSyncData
+ */
+ unsigned int flags;
+
+ /** Event data; content depends on flags. */
+ union {
+ struct NT_ConnectionInfo connInfo;
+ struct NT_TopicInfo topicInfo;
+ struct NT_ValueEventData valueData;
+ struct NT_LogMessage logMessage;
+ struct NT_TimeSyncEventData timeSyncData;
+ } data;
+};
+
+/** NetworkTables publish/subscribe options. */
+struct NT_PubSubOptions {
+ /**
+ * Structure size. Must be set to sizeof(NT_PubSubOptions).
+ */
+ unsigned int structSize;
+
+ /**
+ * Polling storage size for a subscription. Specifies the maximum number of
+ * updates NetworkTables should store between calls to the subscriber's
+ * ReadQueue() function. If zero, defaults to 1 if sendAll is false, 20 if
+ * sendAll is true.
+ */
+ unsigned int pollStorage;
+
+ /**
+ * How frequently changes will be sent over the network, in seconds.
+ * NetworkTables may send more frequently than this (e.g. use a combined
+ * minimum period for all values) or apply a restricted range to this value.
+ * The default is 100 ms.
+ */
+ double periodic;
+
+ /**
+ * For subscriptions, if non-zero, value updates for ReadQueue() are not
+ * queued for this publisher.
+ */
+ NT_Publisher excludePublisher;
+
+ /**
+ * Send all value changes over the network.
+ */
+ NT_Bool sendAll;
+
+ /**
+ * For subscriptions, don't ask for value changes (only topic announcements).
+ */
+ NT_Bool topicsOnly;
+
+ /**
+ * Perform prefix match on subscriber topic names. Is ignored/overridden by
+ * Subscribe() functions; only present in struct for the purposes of getting
+ * information about subscriptions.
+ */
+ NT_Bool prefixMatch;
+
+ /**
+ * Preserve duplicate value changes (rather than ignoring them).
+ */
+ NT_Bool keepDuplicates;
+
+ /**
+ * For subscriptions, if remote value updates should not be queued for
+ * ReadQueue(). See also disableLocal.
+ */
+ NT_Bool disableRemote;
+
+ /**
+ * For subscriptions, if local value updates should not be queued for
+ * ReadQueue(). See also disableRemote.
+ */
+ NT_Bool disableLocal;
+
+ /**
+ * For entries, don't queue (for ReadQueue) value updates for the entry's
+ * internal publisher.
+ */
+ NT_Bool excludeSelf;
+};
+
/**
* @defgroup ntcore_instance_cfunc Instance Functions
* @{
@@ -319,25 +417,6 @@
NT_Entry NT_GetEntry(NT_Inst inst, const char* name, size_t name_len);
/**
- * Get Entry Handles.
- *
- * Returns an array of entry handles. The results are optionally
- * filtered by string prefix and entry type to only return a subset of all
- * entries.
- *
- * @param inst NetworkTable instance
- * @param prefix entry name required prefix; only entries whose name starts
- * with this string are returned
- * @param prefix_len length of prefix in bytes
- * @param types bitmask of NT_Type values; 0 is treated specially as a
- * "don't care"
- * @param count stores number of entry handles returned
- * @return Array of entry handles.
- */
-NT_Entry* NT_GetEntries(NT_Inst inst, const char* prefix, size_t prefix_len,
- unsigned int types, size_t* count);
-
-/**
* Gets the name of the specified entry.
* Returns an empty string if the handle is invalid.
*
@@ -406,21 +485,6 @@
NT_Bool NT_SetEntryValue(NT_Entry entry, const struct NT_Value* value);
/**
- * Set Entry Type and Value.
- *
- * Sets new entry value. If type of new value differs from the type of the
- * currently stored entry, the currently stored entry type is overridden
- * (generally this will generate an Entry Assignment message).
- *
- * This is NOT the preferred method to update a value; generally
- * NT_SetEntryValue() should be used instead, with appropriate error handling.
- *
- * @param entry entry handle
- * @param value new entry value
- */
-void NT_SetEntryTypeValue(NT_Entry entry, const struct NT_Value* value);
-
-/**
* Set Entry Flags.
*
* @param entry entry handle
@@ -437,578 +501,544 @@
unsigned int NT_GetEntryFlags(NT_Entry entry);
/**
- * Delete Entry.
+ * Read Entry Queue.
*
- * Deletes an entry. This is a new feature in version 3.0 of the protocol,
- * so this may not have an effect if any other node in the network is not
- * version 3.0 or newer.
+ * Returns new entry values since last call. The returned array must be freed
+ * using NT_DisposeValueArray().
*
- * Note: NT_GetConnections() can be used to determine the protocol version
- * of direct remote connection(s), but this is not sufficient to determine
- * if all nodes in the network are version 3.0 or newer.
- *
- * @param entry entry handle
+ * @param subentry subscriber or entry handle
+ * @param count count of items in returned array (output)
+ * @return entry value array; returns NULL and count=0 if no new values
*/
-void NT_DeleteEntry(NT_Entry entry);
+struct NT_Value* NT_ReadQueueValue(NT_Handle subentry, size_t* count);
+
+/** @} */
/**
- * Delete All Entries.
- *
- * Deletes ALL table entries. This is a new feature in version 3.0 of the
- * so this may not have an effect if any other node in the network is not
- * version 3.0 or newer.
- *
- * Note: NT_GetConnections() can be used to determine the protocol version
- * of direct remote connection(s), but this is not sufficient to determine
- * if all nodes in the network are version 3.0 or newer.
- *
- * @param inst instance handle
+ * @defgroup ntcore_topic_cfunc Topic Functions
+ * @{
*/
-void NT_DeleteAllEntries(NT_Inst inst);
/**
- * Get Entry Information.
+ * Get Published Topic Handles.
*
- * Returns an array of entry information (entry handle, name, entry type,
- * and timestamp of last change to type/value). The results are optionally
- * filtered by string prefix and entry type to only return a subset of all
- * entries.
+ * Returns an array of topic handles. The results are optionally
+ * filtered by string prefix and type to only return a subset of all
+ * topics.
*
* @param inst instance handle
- * @param prefix entry name required prefix; only entries whose name
+ * @param prefix name required prefix; only topics whose name
* starts with this string are returned
* @param prefix_len length of prefix in bytes
* @param types bitmask of NT_Type values; 0 is treated specially
* as a "don't care"
* @param count output parameter; set to length of returned array
- * @return Array of entry information.
+ * @return Array of topic handles.
*/
-struct NT_EntryInfo* NT_GetEntryInfo(NT_Inst inst, const char* prefix,
- size_t prefix_len, unsigned int types,
- size_t* count);
+NT_Topic* NT_GetTopics(NT_Inst inst, const char* prefix, size_t prefix_len,
+ unsigned int types, size_t* count);
/**
- * Get Entry Information.
+ * Get Published Topic Handles.
*
- * Returns information about an entry (name, entry type,
- * and timestamp of last change to type/value).
+ * Returns an array of topic handles. The results are optionally
+ * filtered by string prefix and type to only return a subset of all
+ * topics.
*
- * @param entry entry handle
- * @param info entry information (output)
+ * @param inst instance handle
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param prefix_len length of prefix in bytes
+ * @param types array of type strings
+ * @param types_len number of elements in types array
+ * @param count output parameter; set to length of returned array
+ * @return Array of topic handles.
+ */
+NT_Topic* NT_GetTopicsStr(NT_Inst inst, const char* prefix, size_t prefix_len,
+ const char* const* types, size_t types_len,
+ size_t* count);
+
+/**
+ * Get Topics.
+ *
+ * Returns an array of topic information (handle, name, type). The results are
+ * optionally filtered by string prefix and type to only return a subset
+ * of all topics.
+ *
+ * @param inst instance handle
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param prefix_len length of prefix in bytes
+ * @param types bitmask of NT_Type values; 0 is treated specially
+ * as a "don't care"
+ * @param count output parameter; set to length of returned array
+ * @return Array of topic information.
+ */
+struct NT_TopicInfo* NT_GetTopicInfos(NT_Inst inst, const char* prefix,
+ size_t prefix_len, unsigned int types,
+ size_t* count);
+
+/**
+ * Get Topics.
+ *
+ * Returns an array of topic information (handle, name, type). The results are
+ * optionally filtered by string prefix and type to only return a subset
+ * of all topics.
+ *
+ * @param inst instance handle
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param prefix_len length of prefix in bytes
+ * @param types array of type strings
+ * @param types_len number of elements in types array
+ * @param count output parameter; set to length of returned array
+ * @return Array of topic information.
+ */
+struct NT_TopicInfo* NT_GetTopicInfosStr(NT_Inst inst, const char* prefix,
+ size_t prefix_len,
+ const char* const* types,
+ size_t types_len, size_t* count);
+
+/**
+ * Gets Topic Information.
+ *
+ * Returns information about a topic (name and type).
+ *
+ * @param topic handle
+ * @param info information (output)
* @return True if successful, false on error.
*/
-NT_Bool NT_GetEntryInfoHandle(NT_Entry entry, struct NT_EntryInfo* info);
+NT_Bool NT_GetTopicInfo(NT_Topic topic, struct NT_TopicInfo* info);
+
+/**
+ * Gets Topic Handle.
+ *
+ * Returns topic handle.
+ *
+ * @param inst instance handle
+ * @param name topic name
+ * @param name_len length of topic name in bytes
+ * @return Topic handle.
+ */
+NT_Topic NT_GetTopic(NT_Inst inst, const char* name, size_t name_len);
+
+/**
+ * Gets the name of the specified topic.
+ *
+ * @param topic topic handle
+ * @param name_len length of topic name (output)
+ * @return Topic name; returns NULL and name_len=0 if the handle is invalid.
+ */
+char* NT_GetTopicName(NT_Topic topic, size_t* name_len);
+
+/**
+ * Gets the type for the specified topic, or unassigned if non existent.
+ *
+ * @param topic topic handle
+ * @return Topic type
+ */
+enum NT_Type NT_GetTopicType(NT_Topic topic);
+
+/**
+ * Gets the type string for the specified topic. This may have more information
+ * than the numeric type (especially for raw values).
+ *
+ * @param topic topic handle
+ * @param type_len length of type string (output)
+ * @return Topic type string; returns NULL if non-existent
+ */
+char* NT_GetTopicTypeString(NT_Topic topic, size_t* type_len);
+
+/**
+ * Sets the persistent property of a topic. If true, the stored value is
+ * persistent through server restarts.
+ *
+ * @param topic topic handle
+ * @param value True for persistent, false for not persistent.
+ */
+void NT_SetTopicPersistent(NT_Topic topic, NT_Bool value);
+
+/**
+ * Gets the persistent property of a topic.
+ *
+ * @param topic topic handle
+ * @return persistent property value
+ */
+NT_Bool NT_GetTopicPersistent(NT_Topic topic);
+
+/**
+ * Sets the retained property of a topic. If true, the server retains the
+ * topic even when there are no publishers.
+ *
+ * @param topic topic handle
+ * @param value new retained property value
+ */
+void NT_SetTopicRetained(NT_Topic topic, NT_Bool value);
+
+/**
+ * Gets the retained property of a topic.
+ *
+ * @param topic topic handle
+ * @return retained property value
+ */
+NT_Bool NT_GetTopicRetained(NT_Topic topic);
+
+/**
+ * Determine if topic exists (e.g. has at least one publisher).
+ *
+ * @param handle Topic, entry, or subscriber handle.
+ * @return True if topic exists.
+ */
+NT_Bool NT_GetTopicExists(NT_Handle handle);
+
+/**
+ * Gets the current value of a property (as a JSON string).
+ *
+ * @param topic topic handle
+ * @param name property name
+ * @param len length of returned string (output)
+ * @return JSON string; empty string if the property does not exist.
+ */
+char* NT_GetTopicProperty(NT_Topic topic, const char* name, size_t* len);
+
+/**
+ * Sets a property value.
+ *
+ * @param topic topic handle
+ * @param name property name
+ * @param value property value (JSON string)
+ */
+NT_Bool NT_SetTopicProperty(NT_Topic topic, const char* name,
+ const char* value);
+
+/**
+ * Deletes a property. Has no effect if the property does not exist.
+ *
+ * @param topic topic handle
+ * @param name property name
+ */
+void NT_DeleteTopicProperty(NT_Topic topic, const char* name);
+
+/**
+ * Gets all topic properties as a JSON string. Each key in the object
+ * is the property name, and the corresponding value is the property value.
+ *
+ * @param topic topic handle
+ * @param len length of returned string (output)
+ * @return JSON string
+ */
+char* NT_GetTopicProperties(NT_Topic topic, size_t* len);
+
+/**
+ * Updates multiple topic properties. Each key in the passed-in JSON object is
+ * the name of the property to add/update, and the corresponding value is the
+ * property value to set for that property. Null values result in deletion
+ * of the corresponding property.
+ *
+ * @param topic topic handle
+ * @param properties JSON object string with keys to add/update/delete
+ * @return False if properties are not a valid JSON object
+ */
+NT_Bool NT_SetTopicProperties(NT_Topic topic, const char* properties);
+
+/**
+ * Creates a new subscriber to value changes on a topic.
+ *
+ * @param topic topic handle
+ * @param type expected type
+ * @param typeStr expected type string
+ * @param options subscription options
+ * @return Subscriber handle
+ */
+NT_Subscriber NT_Subscribe(NT_Topic topic, enum NT_Type type,
+ const char* typeStr,
+ const struct NT_PubSubOptions* options);
+
+/**
+ * Stops subscriber.
+ *
+ * @param sub subscriber handle
+ */
+void NT_Unsubscribe(NT_Subscriber sub);
+
+/**
+ * Creates a new publisher to a topic.
+ *
+ * @param topic topic handle
+ * @param type type
+ * @param typeStr type string
+ * @param options publish options
+ * @return Publisher handle
+ */
+NT_Publisher NT_Publish(NT_Topic topic, enum NT_Type type, const char* typeStr,
+ const struct NT_PubSubOptions* options);
+
+/**
+ * Creates a new publisher to a topic.
+ *
+ * @param topic topic handle
+ * @param type type
+ * @param typeStr type string
+ * @param properties initial properties (JSON object)
+ * @param options publish options
+ * @return Publisher handle
+ */
+NT_Publisher NT_PublishEx(NT_Topic topic, enum NT_Type type,
+ const char* typeStr, const char* properties,
+ const struct NT_PubSubOptions* options);
+
+/**
+ * Stops publisher.
+ *
+ * @param pubentry publisher/entry handle
+ */
+void NT_Unpublish(NT_Handle pubentry);
+
+/**
+ * @brief Creates a new entry (subscriber and weak publisher) to a topic.
+ *
+ * @param topic topic handle
+ * @param type type
+ * @param typeStr type string
+ * @param options publish options
+ * @return Entry handle
+ */
+NT_Entry NT_GetEntryEx(NT_Topic topic, enum NT_Type type, const char* typeStr,
+ const struct NT_PubSubOptions* options);
+
+/**
+ * Stops entry subscriber/publisher.
+ *
+ * @param entry entry handle
+ */
+void NT_ReleaseEntry(NT_Entry entry);
+
+/**
+ * Stops entry/subscriber/publisher.
+ *
+ * @param pubsubentry entry/subscriber/publisher handle
+ */
+void NT_Release(NT_Handle pubsubentry);
+
+/**
+ * Gets the topic handle from an entry/subscriber/publisher handle.
+ *
+ * @param pubsubentry entry/subscriber/publisher handle
+ * @return Topic handle
+ */
+NT_Topic NT_GetTopicFromHandle(NT_Handle pubsubentry);
/** @} */
/**
- * @defgroup ntcore_entrylistener_cfunc Entry Listener Functions
+ * @defgroup ntcore_advancedsub_cfunc Advanced Subscriber Functions
* @{
*/
/**
- * Entry listener callback function.
- * Called when a key-value pair is changed.
+ * Subscribes to multiple topics based on one or more topic name prefixes. Can
+ * be used in combination with a Value Listener or ReadQueueValue() to get value
+ * changes across all matching topics.
*
- * @param data data pointer provided to callback creation function
- * @param event event information
+ * @param inst instance handle
+ * @param prefixes topic name prefixes
+ * @param prefixes_len number of elements in prefixes array
+ * @param options subscriber options
+ * @return subscriber handle
*/
-typedef void (*NT_EntryListenerCallback)(
- void* data, const struct NT_EntryNotification* event);
+NT_MultiSubscriber NT_SubscribeMultiple(NT_Inst inst,
+ const struct NT_String* prefixes,
+ size_t prefixes_len,
+ const struct NT_PubSubOptions* options);
/**
- * Add a listener for all entries starting with a certain prefix.
+ * Unsubscribes a multi-subscriber.
*
- * @param inst instance handle
- * @param prefix UTF-8 string prefix
- * @param prefix_len length of prefix in bytes
- * @param data data pointer to pass to callback
- * @param callback listener to add
- * @param flags NT_NotifyKind bitmask
- * @return Listener handle
+ * @param sub multi-subscriber handle
*/
-NT_EntryListener NT_AddEntryListener(NT_Inst inst, const char* prefix,
- size_t prefix_len, void* data,
- NT_EntryListenerCallback callback,
- unsigned int flags);
-
-/**
- * Add a listener for a single entry.
- *
- * @param entry entry handle
- * @param data data pointer to pass to callback
- * @param callback listener to add
- * @param flags NT_NotifyKind bitmask
- * @return Listener handle
- */
-NT_EntryListener NT_AddEntryListenerSingle(NT_Entry entry, void* data,
- NT_EntryListenerCallback callback,
- unsigned int flags);
-
-/**
- * Create a entry listener poller.
- *
- * A poller provides a single queue of poll events. Events linked to this
- * poller (using NT_AddPolledEntryListener()) will be stored in the queue and
- * must be collected by calling NT_PollEntryListener().
- * The returned handle must be destroyed with NT_DestroyEntryListenerPoller().
- *
- * @param inst instance handle
- * @return poller handle
- */
-NT_EntryListenerPoller NT_CreateEntryListenerPoller(NT_Inst inst);
-
-/**
- * Destroy a entry listener poller. This will abort any blocked polling
- * call and prevent additional events from being generated for this poller.
- *
- * @param poller poller handle
- */
-void NT_DestroyEntryListenerPoller(NT_EntryListenerPoller poller);
-
-/**
- * Create a polled entry listener.
- * The caller is responsible for calling NT_PollEntryListener() to poll.
- *
- * @param poller poller handle
- * @param prefix UTF-8 string prefix
- * @param prefix_len Length of UTF-8 string prefix
- * @param flags NT_NotifyKind bitmask
- * @return Listener handle
- */
-NT_EntryListener NT_AddPolledEntryListener(NT_EntryListenerPoller poller,
- const char* prefix,
- size_t prefix_len,
- unsigned int flags);
-
-/**
- * Create a polled entry listener.
- * The caller is responsible for calling NT_PollEntryListener() to poll.
- *
- * @param poller poller handle
- * @param entry entry handle
- * @param flags NT_NotifyKind bitmask
- * @return Listener handle
- */
-NT_EntryListener NT_AddPolledEntryListenerSingle(NT_EntryListenerPoller poller,
- NT_Entry entry,
- unsigned int flags);
-
-/**
- * Get the next entry listener event. This blocks until the next event occurs.
- *
- * This is intended to be used with NT_AddPolledEntryListener(void); entry
- * listeners created using NT_AddEntryListener() will not be serviced through
- * this function.
- *
- * @param poller poller handle
- * @param len length of returned array (output)
- * @return Array of information on the entry listener events. Returns NULL if
- * an erroroccurred (e.g. the instance was invalid or is shutting down).
- */
-struct NT_EntryNotification* NT_PollEntryListener(NT_EntryListenerPoller poller,
- size_t* len);
-
-/**
- * Get the next entry listener event. This blocks until the next event occurs
- * or it times out. This is intended to be used with
- * NT_AddPolledEntryListener(); entry listeners created using
- * NT_AddEntryListener() will not be serviced through this function.
- *
- * @param poller poller handle
- * @param len length of returned array (output)
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Array of information on the entry listener events. If NULL is
- * returned and timed_out is also false, an error occurred (e.g. the
- * instance was invalid or is shutting down).
- */
-struct NT_EntryNotification* NT_PollEntryListenerTimeout(
- NT_EntryListenerPoller poller, size_t* len, double timeout,
- NT_Bool* timed_out);
-
-/**
- * Cancel a PollEntryListener call. This wakes up a call to
- * PollEntryListener for this poller and causes it to immediately return
- * an empty array.
- *
- * @param poller poller handle
- */
-void NT_CancelPollEntryListener(NT_EntryListenerPoller poller);
-
-/**
- * Remove an entry listener.
- *
- * @param entry_listener Listener handle to remove
- */
-void NT_RemoveEntryListener(NT_EntryListener entry_listener);
-
-/**
- * Wait for the entry listener queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the entry listener
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
-NT_Bool NT_WaitForEntryListenerQueue(NT_Inst inst, double timeout);
+void NT_UnsubscribeMultiple(NT_MultiSubscriber sub);
/** @} */
/**
- * @defgroup ntcore_connectionlistener_cfunc Connection Listener Functions
+ * @defgroup ntcore_listener_cfunc Listener Functions
* @{
*/
/**
- * Connection listener callback function.
- * Called when a network connection is made or lost.
+ * Event listener callback function.
*
* @param data data pointer provided to callback creation function
* @param event event info
*/
-typedef void (*NT_ConnectionListenerCallback)(
- void* data, const struct NT_ConnectionNotification* event);
+typedef void (*NT_ListenerCallback)(void* data, const struct NT_Event* event);
/**
- * Add a connection listener.
+ * Creates a listener poller.
*
- * @param inst instance handle
- * @param data data pointer to pass to callback
- * @param callback listener to add
- * @param immediate_notify notify listener of all existing connections
- * @return Listener handle
- */
-NT_ConnectionListener NT_AddConnectionListener(
- NT_Inst inst, void* data, NT_ConnectionListenerCallback callback,
- NT_Bool immediate_notify);
-
-/**
- * Create a connection listener poller.
* A poller provides a single queue of poll events. Events linked to this
- * poller (using NT_AddPolledConnectionListener()) will be stored in the queue
- * and must be collected by calling NT_PollConnectionListener().
- * The returned handle must be destroyed with
- * NT_DestroyConnectionListenerPoller().
+ * poller (using NT_AddPolledXListener()) will be stored in the queue and
+ * must be collected by calling NT_ReadListenerQueue().
+ * The returned handle must be destroyed with NT_DestroyListenerPoller().
*
* @param inst instance handle
* @return poller handle
*/
-NT_ConnectionListenerPoller NT_CreateConnectionListenerPoller(NT_Inst inst);
+NT_ListenerPoller NT_CreateListenerPoller(NT_Inst inst);
/**
- * Destroy a connection listener poller. This will abort any blocked polling
+ * Destroys a listener poller. This will abort any blocked polling
* call and prevent additional events from being generated for this poller.
*
* @param poller poller handle
*/
-void NT_DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller);
+void NT_DestroyListenerPoller(NT_ListenerPoller poller);
/**
- * Create a polled connection listener.
- * The caller is responsible for calling NT_PollConnectionListener() to poll.
- *
- * @param poller poller handle
- * @param immediate_notify notify listener of all existing connections
- */
-NT_ConnectionListener NT_AddPolledConnectionListener(
- NT_ConnectionListenerPoller poller, NT_Bool immediate_notify);
-
-/**
- * Get the next connection event. This blocks until the next connect or
- * disconnect occurs. This is intended to be used with
- * NT_AddPolledConnectionListener(); connection listeners created using
- * NT_AddConnectionListener() will not be serviced through this function.
+ * Read notifications.
*
* @param poller poller handle
* @param len length of returned array (output)
- * @return Array of information on the connection events. Only returns NULL
- * if an error occurred (e.g. the instance was invalid or is shutting
- * down).
+ * @return Array of events. Returns NULL and len=0 if no events since last
+ * call.
*/
-struct NT_ConnectionNotification* NT_PollConnectionListener(
- NT_ConnectionListenerPoller poller, size_t* len);
+struct NT_Event* NT_ReadListenerQueue(NT_ListenerPoller poller, size_t* len);
/**
- * Get the next connection event. This blocks until the next connect or
- * disconnect occurs or it times out. This is intended to be used with
- * NT_AddPolledConnectionListener(); connection listeners created using
- * NT_AddConnectionListener() will not be serviced through this function.
+ * Removes a listener.
*
- * @param poller poller handle
- * @param len length of returned array (output)
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Array of information on the connection events. If NULL is returned
- * and timed_out is also false, an error occurred (e.g. the instance
- * was invalid or is shutting down).
+ * @param listener Listener handle to remove
*/
-struct NT_ConnectionNotification* NT_PollConnectionListenerTimeout(
- NT_ConnectionListenerPoller poller, size_t* len, double timeout,
- NT_Bool* timed_out);
+void NT_RemoveListener(NT_Listener listener);
/**
- * Cancel a PollConnectionListener call. This wakes up a call to
- * PollConnectionListener for this poller and causes it to immediately return
- * an empty array.
+ * Wait for the listener queue to be empty. This is primarily useful
+ * for deterministic testing. This blocks until either the listener
+ * queue is empty (e.g. there are no more events that need to be passed along to
+ * callbacks or poll queues) or the timeout expires.
*
- * @param poller poller handle
- */
-void NT_CancelPollConnectionListener(NT_ConnectionListenerPoller poller);
-
-/**
- * Remove a connection listener.
- *
- * @param conn_listener Listener handle to remove
- */
-void NT_RemoveConnectionListener(NT_ConnectionListener conn_listener);
-
-/**
- * Wait for the connection listener queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the connection listener
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
+ * @param handle handle
+ * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a
+ * negative value to block indefinitely
* @return False if timed out, otherwise true.
*/
-NT_Bool NT_WaitForConnectionListenerQueue(NT_Inst inst, double timeout);
-
-/** @} */
+NT_Bool NT_WaitForListenerQueue(NT_Handle handle, double timeout);
/**
- * @defgroup ntcore_rpc_cfunc Remote Procedure Call Functions
- * @{
+ * Create a listener for changes to topics with names that start with
+ * the given prefix. This creates a corresponding internal subscriber with the
+ * lifetime of the listener.
+ *
+ * @param inst Instance handle
+ * @param prefix Topic name string prefix
+ * @param prefix_len Length of topic name string prefix
+ * @param mask Bitmask of NT_EventFlags values (only topic and value events will
+ * be generated)
+ * @param data Data passed to callback function
+ * @param callback Listener function
+ * @return Listener handle
*/
+NT_Listener NT_AddListenerSingle(NT_Inst inst, const char* prefix,
+ size_t prefix_len, unsigned int mask,
+ void* data, NT_ListenerCallback callback);
/**
- * Remote Procedure Call (RPC) callback function.
+ * Create a listener for changes to topics with names that start with any of
+ * the given prefixes. This creates a corresponding internal subscriber with the
+ * lifetime of the listener.
*
- * @param data data pointer provided to NT_CreateRpc()
- * @param call call information
- *
- * Note: NT_PostRpcResponse() must be called by the callback to provide a
- * response to the call.
+ * @param inst Instance handle
+ * @param prefixes Topic name string prefixes
+ * @param prefixes_len Number of elements in prefixes array
+ * @param mask Bitmask of NT_EventFlags values (only topic and value events will
+ * be generated)
+ * @param data Data passed to callback function
+ * @param callback Listener function
+ * @return Listener handle
*/
-typedef void (*NT_RpcCallback)(void* data, const struct NT_RpcAnswer* call);
+NT_Listener NT_AddListenerMultiple(NT_Inst inst,
+ const struct NT_String* prefixes,
+ size_t prefixes_len, unsigned int mask,
+ void* data, NT_ListenerCallback callback);
/**
- * Create a callback-based RPC entry point. Only valid to use on the server.
- * The callback function will be called when the RPC is called.
+ * Create a listener.
*
- * @param entry entry handle of RPC entry
- * @param def RPC definition
- * @param def_len length of def in bytes
- * @param data data pointer to pass to callback function
- * @param callback callback function
+ * Some combinations of handle and mask have no effect:
+ * - connection and log message events are only generated on instances
+ * - topic and value events are only generated on non-instances
+ *
+ * Adding value and topic events on a topic will create a corresponding internal
+ * subscriber with the lifetime of the listener.
+ *
+ * Adding a log message listener through this function will only result in
+ * events at NT_LOG_INFO or higher; for more customized settings, use
+ * NT_AddLogger().
+ *
+ * @param handle Handle
+ * @param mask Bitmask of NT_EventFlags values
+ * @param data Data passed to callback function
+ * @param callback Listener function
+ * @return Listener handle
*/
-void NT_CreateRpc(NT_Entry entry, const char* def, size_t def_len, void* data,
- NT_RpcCallback callback);
+NT_Listener NT_AddListener(NT_Handle handle, unsigned int mask, void* data,
+ NT_ListenerCallback callback);
/**
- * Create a RPC call poller. Only valid to use on the server.
+ * Creates a polled topic listener. This creates a corresponding internal
+ * subscriber with the lifetime of the listener.
+ * The caller is responsible for calling NT_ReadListenerQueue() to poll.
*
- * A poller provides a single queue of poll events. Events linked to this
- * poller (using NT_CreatePolledRpc()) will be stored in the queue and must be
- * collected by calling NT_PollRpc() or NT_PollRpcTimeout().
- * The returned handle must be destroyed with NT_DestroyRpcCallPoller().
- *
- * @param inst instance handle
- * @return poller handle
+ * @param poller poller handle
+ * @param prefix UTF-8 string prefix
+ * @param prefix_len Length of UTF-8 string prefix
+ * @param mask NT_EventFlags bitmask (only topic and value events
+ * will be generated)
+ * @return Listener handle
*/
-NT_RpcCallPoller NT_CreateRpcCallPoller(NT_Inst inst);
+NT_Listener NT_AddPolledListenerSingle(NT_ListenerPoller poller,
+ const char* prefix, size_t prefix_len,
+ unsigned int mask);
/**
- * Destroy a RPC call poller. This will abort any blocked polling call and
- * prevent additional events from being generated for this poller.
+ * Creates a polled topic listener. This creates a corresponding internal
+ * subscriber with the lifetime of the listener.
+ * The caller is responsible for calling NT_ReadListenerQueue() to poll.
*
- * @param poller poller handle
+ * @param poller poller handle
+ * @param prefixes array of UTF-8 string prefixes
+ * @param prefixes_len Length of prefixes array
+ * @param mask NT_EventFlags bitmask (only topic and value events
+ * will be generated)
+ * @return Listener handle
*/
-void NT_DestroyRpcCallPoller(NT_RpcCallPoller poller);
+NT_Listener NT_AddPolledListenerMultiple(NT_ListenerPoller poller,
+ const struct NT_String* prefixes,
+ size_t prefixes_len,
+ unsigned int mask);
/**
- * Create a polled RPC entry point. Only valid to use on the server.
+ * Creates a polled listener.
+ * The caller is responsible for calling NT_ReadListenerQueue() to poll.
*
- * The caller is responsible for calling NT_PollRpc() or NT_PollRpcTimeout()
- * to poll for servicing incoming RPC calls.
+ * Some combinations of handle and mask have no effect:
+ * - connection and log message events are only generated on instances
+ * - topic and value events are only generated on non-instances
*
- * @param entry entry handle of RPC entry
- * @param def RPC definition
- * @param def_len length of def in bytes
- * @param poller poller handle
+ * Adding value and topic events on a topic will create a corresponding internal
+ * subscriber with the lifetime of the listener.
+ *
+ * Adding a log message listener through this function will only result in
+ * events at NT_LOG_INFO or higher; for more customized settings, use
+ * NT_AddPolledLogger().
+ *
+ * @param poller poller handle
+ * @param handle handle
+ * @param mask NT_NotifyKind bitmask
+ * @return Listener handle
*/
-void NT_CreatePolledRpc(NT_Entry entry, const char* def, size_t def_len,
- NT_RpcCallPoller poller);
-
-/**
- * Get the next incoming RPC call. This blocks until the next incoming RPC
- * call is received. This is intended to be used with NT_CreatePolledRpc(void);
- * RPC calls created using NT_CreateRpc() will not be serviced through this
- * function. Upon successful return, NT_PostRpcResponse() must be called to
- * send the return value to the caller. The returned array must be freed
- * using NT_DisposeRpcAnswerArray().
- *
- * @param poller poller handle
- * @param len length of returned array (output)
- * @return Array of RPC call information. Only returns NULL if an error
- * occurred (e.g. the instance was invalid or is shutting down).
- */
-struct NT_RpcAnswer* NT_PollRpc(NT_RpcCallPoller poller, size_t* len);
-
-/**
- * Get the next incoming RPC call. This blocks until the next incoming RPC
- * call is received or it times out. This is intended to be used with
- * NT_CreatePolledRpc(); RPC calls created using NT_CreateRpc() will not be
- * serviced through this function. Upon successful return,
- * NT_PostRpcResponse() must be called to send the return value to the caller.
- * The returned array must be freed using NT_DisposeRpcAnswerArray().
- *
- * @param poller poller handle
- * @param len length of returned array (output)
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Array of RPC call information. If NULL is returned and timed_out
- * is also false, an error occurred (e.g. the instance was invalid or
- * is shutting down).
- */
-struct NT_RpcAnswer* NT_PollRpcTimeout(NT_RpcCallPoller poller, size_t* len,
- double timeout, NT_Bool* timed_out);
-
-/**
- * Cancel a PollRpc call. This wakes up a call to PollRpc for this poller
- * and causes it to immediately return an empty array.
- *
- * @param poller poller handle
- */
-void NT_CancelPollRpc(NT_RpcCallPoller poller);
-
-/**
- * Wait for the incoming RPC call queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the RPC call
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
-NT_Bool NT_WaitForRpcCallQueue(NT_Inst inst, double timeout);
-
-/**
- * Post RPC response (return value) for a polled RPC.
- *
- * The rpc and call parameters should come from the NT_RpcAnswer returned
- * by NT_PollRpc().
- *
- * @param entry entry handle of RPC entry (from NT_RpcAnswer)
- * @param call RPC call handle (from NT_RpcAnswer)
- * @param result result raw data that will be provided to remote caller
- * @param result_len length of result in bytes
- * @return true if the response was posted, otherwise false
- */
-NT_Bool NT_PostRpcResponse(NT_Entry entry, NT_RpcCall call, const char* result,
- size_t result_len);
-
-/**
- * Call a RPC function. May be used on either the client or server.
- *
- * This function is non-blocking. Either NT_GetRpcResult() or
- * NT_CancelRpcResult() must be called to either get or ignore the result of
- * the call.
- *
- * @param entry entry handle of RPC entry
- * @param params parameter
- * @param params_len length of param in bytes
- * @return RPC call handle (for use with NT_GetRpcResult() or
- * NT_CancelRpcResult()).
- */
-NT_RpcCall NT_CallRpc(NT_Entry entry, const char* params, size_t params_len);
-
-/**
- * Get the result (return value) of a RPC call. This function blocks until
- * the result is received.
- *
- * @param entry entry handle of RPC entry
- * @param call RPC call handle returned by NT_CallRpc()
- * @param result_len length of returned result in bytes
- * @return NULL on error, or result.
- */
-char* NT_GetRpcResult(NT_Entry entry, NT_RpcCall call, size_t* result_len);
-
-/**
- * Get the result (return value) of a RPC call. This function blocks until
- * the result is received or it times out.
- *
- * @param entry entry handle of RPC entry
- * @param call RPC call handle returned by NT_CallRpc()
- * @param result_len length of returned result in bytes
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return NULL on error or timeout, or result.
- */
-char* NT_GetRpcResultTimeout(NT_Entry entry, NT_RpcCall call,
- size_t* result_len, double timeout,
- NT_Bool* timed_out);
-
-/**
- * Ignore the result of a RPC call. This function is non-blocking.
- *
- * @param entry entry handle of RPC entry
- * @param call RPC call handle returned by NT_CallRpc()
- */
-void NT_CancelRpcResult(NT_Entry entry, NT_RpcCall call);
-
-/**
- * Pack a RPC version 1 definition.
- *
- * @param def RPC version 1 definition
- * @param packed_len length of return value in bytes
- * @return Raw packed bytes. Use C standard library std::free() to release.
- */
-char* NT_PackRpcDefinition(const struct NT_RpcDefinition* def,
- size_t* packed_len);
-
-/**
- * Unpack a RPC version 1 definition. This can be used for introspection or
- * validation.
- *
- * @param packed raw packed bytes
- * @param packed_len length of packed in bytes
- * @param def RPC version 1 definition (output)
- * @return True if successfully unpacked, false otherwise.
- */
-NT_Bool NT_UnpackRpcDefinition(const char* packed, size_t packed_len,
- struct NT_RpcDefinition* def);
-
-/**
- * Pack RPC values as required for RPC version 1 definition messages.
- *
- * @param values array of values to pack
- * @param values_len length of values
- * @param packed_len length of return value in bytes
- * @return Raw packed bytes. Use C standard library std::free() to release.
- */
-char* NT_PackRpcValues(const struct NT_Value** values, size_t values_len,
- size_t* packed_len);
-
-/**
- * Unpack RPC values as required for RPC version 1 definition messages.
- *
- * @param packed raw packed bytes
- * @param packed_len length of packed in bytes
- * @param types array of data types (as provided in the RPC definition)
- * @param types_len length of types
- * @return Array of NT_Value's.
- */
-struct NT_Value** NT_UnpackRpcValues(const char* packed, size_t packed_len,
- const enum NT_Type* types,
- size_t types_len);
+NT_Listener NT_AddPolledListener(NT_ListenerPoller poller, NT_Handle handle,
+ unsigned int mask);
/** @} */
@@ -1018,17 +1048,6 @@
*/
/**
- * Set the network identity of this node.
- * This is the name used during the initial connection handshake, and is
- * visible through NT_ConnectionInfo on the remote node.
- *
- * @param inst instance handle
- * @param name identity to advertise
- * @param name_len length of name in bytes
- */
-void NT_SetNetworkIdentity(NT_Inst inst, const char* name, size_t name_len);
-
-/**
* Get the current network mode.
*
* @param inst instance handle
@@ -1057,10 +1076,12 @@
* null terminated)
* @param listen_address the address to listen on, or null to listen on any
* address. (UTF-8 string, null terminated)
- * @param port port to communicate over.
+ * @param port3 port to communicate over (NT3)
+ * @param port4 port to communicate over (NT4)
*/
void NT_StartServer(NT_Inst inst, const char* persist_filename,
- const char* listen_address, unsigned int port);
+ const char* listen_address, unsigned int port3,
+ unsigned int port4);
/**
* Stops the server if it is running.
@@ -1070,42 +1091,22 @@
void NT_StopServer(NT_Inst inst);
/**
- * Starts a client. Use NT_SetServer to set the server name and port.
+ * Starts a NT3 client. Use NT_SetServer or NT_SetServerTeam to set the server
+ * name and port.
*
- * @param inst instance handle
+ * @param inst instance handle
+ * @param identity network identity to advertise (cannot be empty string)
*/
-void NT_StartClientNone(NT_Inst inst);
+void NT_StartClient3(NT_Inst inst, const char* identity);
/**
- * Starts a client using the specified server and port
+ * Starts a NT4 client. Use NT_SetServer or NT_SetServerTeam to set the server
+ * name and port.
*
- * @param inst instance handle
- * @param server_name server name (UTF-8 string, null terminated)
- * @param port port to communicate over
+ * @param inst instance handle
+ * @param identity network identity to advertise (cannot be empty string)
*/
-void NT_StartClient(NT_Inst inst, const char* server_name, unsigned int port);
-
-/**
- * Starts a client using the specified (server, port) combinations. The
- * client will attempt to connect to each server in round robin fashion.
- *
- * @param inst instance handle
- * @param count length of the server_names and ports arrays
- * @param server_names array of server names (each a UTF-8 string, null
- * terminated)
- * @param ports array of ports to communicate over (one for each server)
- */
-void NT_StartClientMulti(NT_Inst inst, size_t count, const char** server_names,
- const unsigned int* ports);
-
-/**
- * Starts a client using commonly known robot addresses for the specified team.
- *
- * @param inst instance handle
- * @param team team number
- * @param port port to communicate over
- */
-void NT_StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port);
+void NT_StartClient4(NT_Inst inst, const char* identity);
/**
* Stops the client if it is running.
@@ -1152,7 +1153,7 @@
* server IP address.
*
* @param inst instance handle
- * @param port server port to use in combination with IP from DS
+ * @param port server port to use in combination with IP from DS
*/
void NT_StartDSClient(NT_Inst inst, unsigned int port);
@@ -1164,20 +1165,23 @@
void NT_StopDSClient(NT_Inst inst);
/**
- * Set the periodic update rate.
- * Sets how frequently updates are sent to other nodes over the network.
+ * Flush local updates.
+ *
+ * Forces an immediate flush of all local changes to the client/server.
+ * This does not flush to the network.
+ *
+ * Normally this is done on a regularly scheduled interval.
*
* @param inst instance handle
- * @param interval update interval in seconds (range 0.01 to 1.0)
*/
-void NT_SetUpdateRate(NT_Inst inst, double interval);
+void NT_FlushLocal(NT_Inst inst);
/**
- * Flush Entries.
+ * Flush to network.
*
* Forces an immediate flush of all local entry changes to network.
- * Normally this is done on a regularly scheduled interval (see
- * NT_SetUpdateRate()).
+ * Normally this is done on a regularly scheduled interval (set
+ * by update rates on individual publishers).
*
* Note: flushes are rate limited to avoid excessive network traffic. If
* the time between calls is too short, the flush will occur after the minimum
@@ -1208,64 +1212,21 @@
*/
NT_Bool NT_IsConnected(NT_Inst inst);
-/** @} */
-
/**
- * @defgroup ntcore_file_cfunc File Save/Load Functions
- * @{
- */
-
-/**
- * Save persistent values to a file. The server automatically does this,
- * but this function provides a way to save persistent values in the same
- * format to a file on either a client or a server.
+ * Get the time offset between server time and local time. Add this value to
+ * local time to get the estimated equivalent server time. In server mode, this
+ * always returns a valid value of 0. In client mode, this returns the time
+ * offset only if the client and server are connected and have exchanged
+ * synchronization messages. Note the time offset may change over time as it is
+ * periodically updated; to receive updates as events, add a listener to the
+ * "time sync" event.
*
- * @param inst instance handle
- * @param filename filename
- * @return error string, or NULL if successful
+ * @param inst instance handle
+ * @param valid set to true if the return value is valid, false otherwise
+ * (output)
+ * @return Time offset in microseconds (if valid is set to true)
*/
-const char* NT_SavePersistent(NT_Inst inst, const char* filename);
-
-/**
- * Load persistent values from a file. The server automatically does this
- * at startup, but this function provides a way to restore persistent values
- * in the same format from a file at any time on either a client or a server.
- *
- * @param inst instance handle
- * @param filename filename
- * @param warn callback function for warnings
- * @return error string, or NULL if successful
- */
-const char* NT_LoadPersistent(NT_Inst inst, const char* filename,
- void (*warn)(size_t line, const char* msg));
-
-/**
- * Save table values to a file. The file format used is identical to
- * that used for SavePersistent.
- *
- * @param inst instance handle
- * @param filename filename
- * @param prefix save only keys starting with this prefix
- * @param prefix_len length of prefix in bytes
- * @return error string, or nullptr if successful
- */
-const char* NT_SaveEntries(NT_Inst inst, const char* filename,
- const char* prefix, size_t prefix_len);
-
-/**
- * Load table values from a file. The file format used is identical to
- * that used for SavePersistent / LoadPersistent.
- *
- * @param inst instance handle
- * @param filename filename
- * @param prefix load only keys starting with this prefix
- * @param prefix_len length of prefix in bytes
- * @param warn callback function for warnings
- * @return error string, or nullptr if successful
- */
-const char* NT_LoadEntries(NT_Inst inst, const char* filename,
- const char* prefix, size_t prefix_len,
- void (*warn)(size_t line, const char* msg));
+int64_t NT_GetServerTimeOffset(NT_Inst inst, NT_Bool* valid);
/** @} */
@@ -1305,12 +1266,16 @@
void NT_InitString(struct NT_String* str);
/**
- * Disposes an entry handle array.
+ * Frees an array of NT_Values.
*
- * @param arr pointer to the array to dispose
+ * @param arr pointer to the value array to free
* @param count number of elements in the array
+ *
+ * Note that the individual NT_Values in the array should NOT be
+ * freed before calling this. This function will free all the values
+ * individually.
*/
-void NT_DisposeEntryArray(NT_Entry* arr, size_t count);
+void NT_DisposeValueArray(struct NT_Value* arr, size_t count);
/**
* Disposes a connection info array.
@@ -1321,97 +1286,56 @@
void NT_DisposeConnectionInfoArray(struct NT_ConnectionInfo* arr, size_t count);
/**
- * Disposes an entry info array.
+ * Disposes a topic info array.
*
* @param arr pointer to the array to dispose
* @param count number of elements in the array
*/
-void NT_DisposeEntryInfoArray(struct NT_EntryInfo* arr, size_t count);
+void NT_DisposeTopicInfoArray(struct NT_TopicInfo* arr, size_t count);
/**
- * Disposes a single entry info (as returned by NT_GetEntryInfoHandle).
+ * Disposes a single topic info (as returned by NT_GetTopicInfo).
*
* @param info pointer to the info to dispose
*/
-void NT_DisposeEntryInfo(struct NT_EntryInfo* info);
+void NT_DisposeTopicInfo(struct NT_TopicInfo* info);
/**
- * Disposes a Rpc Definition structure.
- *
- * @param def pointer to the struct to dispose
- */
-void NT_DisposeRpcDefinition(struct NT_RpcDefinition* def);
-
-/**
- * Disposes a Rpc Answer array.
+ * Disposes an event array.
*
* @param arr pointer to the array to dispose
* @param count number of elements in the array
*/
-void NT_DisposeRpcAnswerArray(struct NT_RpcAnswer* arr, size_t count);
+void NT_DisposeEventArray(struct NT_Event* arr, size_t count);
/**
- * Disposes a Rpc Answer structure.
+ * Disposes a single event.
*
- * @param answer pointer to the struct to dispose
+ * @param event pointer to the event to dispose
*/
-void NT_DisposeRpcAnswer(struct NT_RpcAnswer* answer);
-
-/**
- * Disposes an entry notification array.
- *
- * @param arr pointer to the array to dispose
- * @param count number of elements in the array
- */
-void NT_DisposeEntryNotificationArray(struct NT_EntryNotification* arr,
- size_t count);
-
-/**
- * Disposes a single entry notification.
- *
- * @param info pointer to the info to dispose
- */
-void NT_DisposeEntryNotification(struct NT_EntryNotification* info);
-
-/**
- * Disposes a connection notification array.
- *
- * @param arr pointer to the array to dispose
- * @param count number of elements in the array
- */
-void NT_DisposeConnectionNotificationArray(
- struct NT_ConnectionNotification* arr, size_t count);
-
-/**
- * Disposes a single connection notification.
- *
- * @param info pointer to the info to dispose
- */
-void NT_DisposeConnectionNotification(struct NT_ConnectionNotification* info);
-
-/**
- * Disposes a log message array.
- *
- * @param arr pointer to the array to dispose
- * @param count number of elements in the array
- */
-void NT_DisposeLogMessageArray(struct NT_LogMessage* arr, size_t count);
-
-/**
- * Disposes a single log message.
- *
- * @param info pointer to the info to dispose
- */
-void NT_DisposeLogMessage(struct NT_LogMessage* info);
+void NT_DisposeEvent(struct NT_Event* event);
/**
* Returns monotonic current time in 1 us increments.
* This is the same time base used for entry and connection timestamps.
- * This function is a compatibility wrapper around WPI_Now().
+ * This function by default simply wraps WPI_Now(), but if NT_SetNow() is
+ * called, this function instead returns the value passed to NT_SetNow();
+ * this can be used to reduce overhead.
*
* @return Timestamp
*/
-uint64_t NT_Now(void);
+int64_t NT_Now(void);
+
+/**
+ * Sets the current timestamp used for timestamping values that do not
+ * provide a timestamp (e.g. a value of 0 is passed). For consistency,
+ * it also results in NT_Now() returning the set value. This should generally
+ * be used only if the overhead of calling WPI_Now() is a concern.
+ * If used, it should be called periodically with the value of WPI_Now().
+ *
+ * @param timestamp timestamp (1 us increments)
+ */
+void NT_SetNow(int64_t timestamp);
/** @} */
@@ -1421,14 +1345,6 @@
*/
/**
- * Log function.
- *
- * @param data data pointer passed to NT_AddLogger()
- * @param msg message information
- */
-typedef void (*NT_LogFunc)(void* data, const struct NT_LogMessage* msg);
-
-/**
* Add logger callback function. By default, log messages are sent to stderr;
* this function sends log messages to the provided callback function instead.
* The callback function will only be called for log messages with level
@@ -1436,97 +1352,28 @@
* messages outside this range will be silently ignored.
*
* @param inst instance handle
- * @param data data pointer to pass to func
- * @param func log callback function
* @param min_level minimum log level
* @param max_level maximum log level
- * @return Logger handle
+ * @param data data pointer to pass to func
+ * @param func listener callback function
+ * @return Listener handle
*/
-NT_Logger NT_AddLogger(NT_Inst inst, void* data, NT_LogFunc func,
- unsigned int min_level, unsigned int max_level);
+NT_Listener NT_AddLogger(NT_Inst inst, unsigned int min_level,
+ unsigned int max_level, void* data,
+ NT_ListenerCallback func);
/**
- * Create a log poller. A poller provides a single queue of poll events.
- * The returned handle must be destroyed with NT_DestroyLoggerPoller().
- *
- * @param inst instance handle
- * @return poller handle
- */
-NT_LoggerPoller NT_CreateLoggerPoller(NT_Inst inst);
-
-/**
- * Destroy a log poller. This will abort any blocked polling call and prevent
- * additional events from being generated for this poller.
- *
- * @param poller poller handle
- */
-void NT_DestroyLoggerPoller(NT_LoggerPoller poller);
-
-/**
- * Set the log level for a log poller. Events will only be generated for
+ * Set the log level for a listener poller. Events will only be generated for
* log messages with level greater than or equal to min_level and less than or
* equal to max_level; messages outside this range will be silently ignored.
*
* @param poller poller handle
* @param min_level minimum log level
* @param max_level maximum log level
- * @return Logger handle
+ * @return Listener handle
*/
-NT_Logger NT_AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
- unsigned int max_level);
-
-/**
- * Get the next log event. This blocks until the next log occurs.
- *
- * @param poller poller handle
- * @param len length of returned array (output)
- * @return Array of information on the log events. Only returns NULL if an
- * error occurred (e.g. the instance was invalid or is shutting down).
- */
-struct NT_LogMessage* NT_PollLogger(NT_LoggerPoller poller, size_t* len);
-
-/**
- * Get the next log event. This blocks until the next log occurs or it times
- * out.
- *
- * @param poller poller handle
- * @param len length of returned array (output)
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Array of information on the log events. If NULL is returned and
- * timed_out is also false, an error occurred (e.g. the instance was
- * invalid or is shutting down).
- */
-struct NT_LogMessage* NT_PollLoggerTimeout(NT_LoggerPoller poller, size_t* len,
- double timeout, NT_Bool* timed_out);
-
-/**
- * Cancel a PollLogger call. This wakes up a call to PollLogger for this
- * poller and causes it to immediately return an empty array.
- *
- * @param poller poller handle
- */
-void NT_CancelPollLogger(NT_LoggerPoller poller);
-
-/**
- * Remove a logger.
- *
- * @param logger Logger handle to remove
- */
-void NT_RemoveLogger(NT_Logger logger);
-
-/**
- * Wait for the incoming log event queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the log event
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
-NT_Bool NT_WaitForLoggerQueue(NT_Inst inst, double timeout);
+NT_Listener NT_AddPolledLogger(NT_ListenerPoller poller, unsigned int min_level,
+ unsigned int max_level);
/** @} */
@@ -1567,6 +1414,32 @@
NT_Bool* NT_AllocateBooleanArray(size_t size);
/**
+ * Allocates an array of ints.
+ * Note that the size is the number of elements, and not the
+ * specific number of bytes to allocate. That is calculated internally.
+ *
+ * @param size the number of elements the array will contain
+ * @return the allocated double array
+ *
+ * After use, the array should be freed using the NT_FreeIntArray()
+ * function.
+ */
+int64_t* NT_AllocateIntegerArray(size_t size);
+
+/**
+ * Allocates an array of floats.
+ * Note that the size is the number of elements, and not the
+ * specific number of bytes to allocate. That is calculated internally.
+ *
+ * @param size the number of elements the array will contain
+ * @return the allocated double array
+ *
+ * After use, the array should be freed using the NT_FreeFloatArray()
+ * function.
+ */
+float* NT_AllocateFloatArray(size_t size);
+
+/**
* Allocates an array of doubles.
* Note that the size is the number of elements, and not the
* specific number of bytes to allocate. That is calculated internally.
@@ -1600,13 +1473,6 @@
void NT_FreeCharArray(char* v_char);
/**
- * Frees an array of doubles.
- *
- * @param v_double pointer to the double array to free
- */
-void NT_FreeDoubleArray(double* v_double);
-
-/**
* Frees an array of booleans.
*
* @param v_boolean pointer to the boolean array to free
@@ -1614,6 +1480,27 @@
void NT_FreeBooleanArray(NT_Bool* v_boolean);
/**
+ * Frees an array of ints.
+ *
+ * @param v_int pointer to the int array to free
+ */
+void NT_FreeIntegerArray(int64_t* v_int);
+
+/**
+ * Frees an array of floats.
+ *
+ * @param v_float pointer to the float array to free
+ */
+void NT_FreeFloatArray(float* v_float);
+
+/**
+ * Frees an array of doubles.
+ *
+ * @param v_double pointer to the double array to free
+ */
+void NT_FreeDoubleArray(double* v_double);
+
+/**
* Frees an array of NT_Strings.
*
* @param v_string pointer to the string array to free
@@ -1654,12 +1541,36 @@
NT_Bool* v_boolean);
/**
+ * Returns the int from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns 0.
+ *
+ * @param value NT_Value struct to get the int from
+ * @param last_change returns time in ms since the last change in the value
+ * @param v_int returns the int assigned to the name
+ * @return 1 if successful, or 0 if value is null or not an int
+ */
+NT_Bool NT_GetValueInteger(const struct NT_Value* value, uint64_t* last_change,
+ int64_t* v_int);
+
+/**
+ * Returns the float from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns 0.
+ *
+ * @param value NT_Value struct to get the float from
+ * @param last_change returns time in ms since the last change in the value
+ * @param v_float returns the float assigned to the name
+ * @return 1 if successful, or 0 if value is null or not a float
+ */
+NT_Bool NT_GetValueFloat(const struct NT_Value* value, uint64_t* last_change,
+ float* v_float);
+
+/**
* Returns the double from the NT_Value.
* If the NT_Value is null, or is assigned to a different type, returns 0.
*
* @param value NT_Value struct to get the double from
* @param last_change returns time in ms since the last change in the value
- * @param v_double returns the boolean assigned to the name
+ * @param v_double returns the double assigned to the name
* @return 1 if successful, or 0 if value is null or not a double
*/
NT_Bool NT_GetValueDouble(const struct NT_Value* value, uint64_t* last_change,
@@ -1696,8 +1607,8 @@
* returned string is a copy of the string in the value, and must be freed
* separately.
*/
-char* NT_GetValueRaw(const struct NT_Value* value, uint64_t* last_change,
- size_t* raw_len);
+uint8_t* NT_GetValueRaw(const struct NT_Value* value, uint64_t* last_change,
+ size_t* raw_len);
/**
* Returns a copy of the boolean array from the NT_Value.
@@ -1717,6 +1628,40 @@
uint64_t* last_change, size_t* arr_size);
/**
+ * Returns a copy of the int array from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns null.
+ *
+ * @param value NT_Value struct to get the int array from
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size returns the number of elements in the array
+ * @return pointer to the int array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeIntArray() function is useful for this purpose.
+ * The returned array is a copy of the array in the value, and must be
+ * freed separately.
+ */
+int64_t* NT_GetValueIntegerArray(const struct NT_Value* value,
+ uint64_t* last_change, size_t* arr_size);
+
+/**
+ * Returns a copy of the float array from the NT_Value.
+ * If the NT_Value is null, or is assigned to a different type, returns null.
+ *
+ * @param value NT_Value struct to get the float array from
+ * @param last_change returns time in ms since the last change in the value
+ * @param arr_size returns the number of elements in the array
+ * @return pointer to the float array, or null if error
+ *
+ * It is the caller's responsibility to free the array once its no longer
+ * needed. The NT_FreeFloatArray() function is useful for this purpose.
+ * The returned array is a copy of the array in the value, and must be
+ * freed separately.
+ */
+float* NT_GetValueFloatArray(const struct NT_Value* value,
+ uint64_t* last_change, size_t* arr_size);
+
+/**
* Returns a copy of the double array from the NT_Value.
* If the NT_Value is null, or is assigned to a different type, returns null.
*
@@ -1753,331 +1698,176 @@
uint64_t* last_change,
size_t* arr_size);
-/**
- * Returns the boolean currently assigned to the entry name.
- * If the entry name is not currently assigned, or is assigned to a
- * different type, returns 0.
- *
- * @param entry entry handle
- * @param last_change returns time in ms since the last change in the value
- * @param v_boolean returns the boolean assigned to the name
- * @return 1 if successful, or 0 if value is unassigned or not a
- * boolean
- */
-NT_Bool NT_GetEntryBoolean(NT_Entry entry, uint64_t* last_change,
- NT_Bool* v_boolean);
-
-/**
- * Returns the double currently assigned to the entry name.
- * If the entry name is not currently assigned, or is assigned to a
- * different type, returns 0.
- *
- * @param entry entry handle
- * @param last_change returns time in ms since the last change in the value
- * @param v_double returns the double assigned to the name
- * @return 1 if successful, or 0 if value is unassigned or not a
- * double
- */
-NT_Bool NT_GetEntryDouble(NT_Entry entry, uint64_t* last_change,
- double* v_double);
-
-/**
- * Returns a copy of the string assigned to the entry name.
- * If the entry name is not currently assigned, or is assigned to a
- * different type, returns null.
- *
- * @param entry entry handle
- * @param last_change returns time in ms since the last change in the value
- * @param str_len returns the length of the string
- * @return pointer to the string (UTF-8), or null if error
- *
- * It is the caller's responsibility to free the string once its no longer
- * needed. The NT_FreeCharArray() function is useful for this purpose.
- */
-char* NT_GetEntryString(NT_Entry entry, uint64_t* last_change, size_t* str_len);
-
-/**
- * Returns a copy of the raw value assigned to the entry name.
- * If the entry name is not currently assigned, or is assigned to a
- * different type, returns null.
- *
- * @param entry entry handle
- * @param last_change returns time in ms since the last change in the value
- * @param raw_len returns the length of the string
- * @return pointer to the raw value (UTF-8), or null if error
- *
- * It is the caller's responsibility to free the raw value once its no longer
- * needed. The NT_FreeCharArray() function is useful for this purpose.
- */
-char* NT_GetEntryRaw(NT_Entry entry, uint64_t* last_change, size_t* raw_len);
-
-/**
- * Returns a copy of the boolean array assigned to the entry name.
- * If the entry name is not currently assigned, or is assigned to a
- * different type, returns null.
- *
- * @param entry entry handle
- * @param last_change returns time in ms since the last change in the value
- * @param arr_size returns the number of elements in the array
- * @return pointer to the boolean array, or null if error
- *
- * It is the caller's responsibility to free the array once its no longer
- * needed. The NT_FreeBooleanArray() function is useful for this purpose.
- */
-NT_Bool* NT_GetEntryBooleanArray(NT_Entry entry, uint64_t* last_change,
- size_t* arr_size);
-
-/**
- * Returns a copy of the double array assigned to the entry name.
- * If the entry name is not currently assigned, or is assigned to a
- * different type, returns null.
- *
- * @param entry entry handle
- * @param last_change returns time in ms since the last change in the value
- * @param arr_size returns the number of elements in the array
- * @return pointer to the double array, or null if error
- *
- * It is the caller's responsibility to free the array once its no longer
- * needed. The NT_FreeDoubleArray() function is useful for this purpose.
- */
-double* NT_GetEntryDoubleArray(NT_Entry entry, uint64_t* last_change,
- size_t* arr_size);
-
-/**
- * Returns a copy of the NT_String array assigned to the entry name.
- * If the entry name is not currently assigned, or is assigned to a
- * different type, returns null.
- *
- * @param entry entry handle
- * @param last_change returns time in ms since the last change in the value
- * @param arr_size returns the number of elements in the array
- * @return pointer to the NT_String array, or null if error
- *
- * It is the caller's responsibility to free the array once its no longer
- * needed. The NT_FreeStringArray() function is useful for this purpose. Note
- * that the individual NT_Strings should not be freed, but the entire array
- * should be freed at once. The NT_FreeStringArray() function will free all the
- * NT_Strings.
- */
-struct NT_String* NT_GetEntryStringArray(NT_Entry entry, uint64_t* last_change,
- size_t* arr_size);
-
+/** @} */
+/** @} */
/** @} */
/**
- * @defgroup ntcore_setdefault_cfunc Set Default Values
+ * @defgroup ntcore_c_meta_api ntcore C meta-topic API
+ *
+ * Meta-topic decoders for C.
+ *
* @{
*/
-/** Set Default Entry Boolean.
- * Sets the default for the specified key to be a boolean.
- * If key exists with same type, does not set value. Otherwise
- * sets value to the default.
- *
- * @param entry entry handle
- * @param time timestamp
- * @param default_boolean value to be set if name does not exist
- * @return 0 on error (value not set), 1 on success
+/**
+ * Subscriber options. Different from PubSubOptions in this reflects only
+ * options that are sent over the network.
*/
-NT_Bool NT_SetDefaultEntryBoolean(NT_Entry entry, uint64_t time,
- NT_Bool default_boolean);
-
-/** Set Default Entry Double.
- * Sets the default for the specified key.
- * If key exists with same type, does not set value. Otherwise
- * sets value to the default.
- *
- * @param entry entry handle
- * @param time timestamp
- * @param default_double value to be set if name does not exist
- * @return 0 on error (value not set), 1 on success
- */
-NT_Bool NT_SetDefaultEntryDouble(NT_Entry entry, uint64_t time,
- double default_double);
-
-/** Set Default Entry String.
- * Sets the default for the specified key.
- * If key exists with same type, does not set value. Otherwise
- * sets value to the default.
- *
- * @param entry entry handle
- * @param time timestamp
- * @param default_value value to be set if name does not exist
- * @param default_len length of value
- * @return 0 on error (value not set), 1 on success
- */
-NT_Bool NT_SetDefaultEntryString(NT_Entry entry, uint64_t time,
- const char* default_value, size_t default_len);
-
-/** Set Default Entry Raw.
- * Sets the default for the specified key.
- * If key exists with same type, does not set value. Otherwise
- * sets value to the default.
- *
- * @param entry entry handle
- * @param time timestamp
- * @param default_value value to be set if name does not exist
- * @param default_len length of value array
- * @return 0 on error (value not set), 1 on success
- */
-NT_Bool NT_SetDefaultEntryRaw(NT_Entry entry, uint64_t time,
- const char* default_value, size_t default_len);
-
-/** Set Default Entry Boolean Array.
- * Sets the default for the specified key.
- * If key exists with same type, does not set value. Otherwise
- * sets value to the default.
- *
- * @param entry entry handle
- * @param time timestamp
- * @param default_value value to be set if name does not exist
- * @param default_size size of value array
- * @return 0 on error (value not set), 1 on success
- */
-NT_Bool NT_SetDefaultEntryBooleanArray(NT_Entry entry, uint64_t time,
- const int* default_value,
- size_t default_size);
-
-/** Set Default Entry Double Array.
- * Sets the default for the specified key.
- * If key exists with same type, does not set value. Otherwise
- * sets value to the default.
- *
- * @param entry entry handle
- * @param time timestamp
- * @param default_value value to be set if name does not exist
- * @param default_size size of value array
- * @return 0 on error (value not set), 1 on success
- */
-NT_Bool NT_SetDefaultEntryDoubleArray(NT_Entry entry, uint64_t time,
- const double* default_value,
- size_t default_size);
-
-/** Set Default Entry String Array.
- * Sets the default for the specified key.
- * If key exists with same type, does not set value. Otherwise
- * sets value to the default.
- *
- * @param entry entry handle
- * @param time timestamp
- * @param default_value value to be set if name does not exist
- * @param default_size size of value array
- * @return 0 on error (value not set), 1 on success
- */
-NT_Bool NT_SetDefaultEntryStringArray(NT_Entry entry, uint64_t time,
- const struct NT_String* default_value,
- size_t default_size);
-
-/** @} */
+struct NT_Meta_SubscriberOptions {
+ double periodic;
+ NT_Bool topicsOnly;
+ NT_Bool sendAll;
+ NT_Bool prefixMatch;
+};
/**
- * @defgroup ntcore_valuesetters_cfunc Entry Value Setters
- * @{
+ * Topic publisher (as published via `$pub$<topic>`).
*/
+struct NT_Meta_TopicPublisher {
+ struct NT_String client;
+ uint64_t pubuid;
+};
-/** Set Entry Boolean
- * Sets an entry boolean. If the entry name is not currently assigned to a
- * boolean, returns error unless the force parameter is set.
+/**
+ * Topic subscriber (as published via `$sub$<topic>`).
+ */
+struct NT_Meta_TopicSubscriber {
+ struct NT_String client;
+ uint64_t subuid;
+ struct NT_Meta_SubscriberOptions options;
+};
+
+/**
+ * Client publisher (as published via `$clientpub$<client>` or `$serverpub`).
+ */
+struct NT_Meta_ClientPublisher {
+ int64_t uid;
+ struct NT_String topic;
+};
+
+/**
+ * Client subscriber (as published via `$clientsub$<client>` or `$serversub`).
+ */
+struct NT_Meta_ClientSubscriber {
+ int64_t uid;
+ size_t topicsCount;
+ struct NT_String* topics;
+ struct NT_Meta_SubscriberOptions options;
+};
+
+/**
+ * Client (as published via `$clients`).
+ */
+struct NT_Meta_Client {
+ struct NT_String id;
+ struct NT_String conn;
+ uint16_t version;
+};
+
+/**
+ * Decodes `$pub$<topic>` meta-topic data.
*
- * @param entry entry handle
- * @param time timestamp
- * @param v_boolean boolean value to set
- * @param force 1 to force the entry to get overwritten, otherwise 0
- * @return 0 on error (type mismatch), 1 on success
+ * @param data data contents
+ * @param size size of data contents
+ * @param count number of elements in returned array (output)
+ * @return Array of TopicPublishers, or NULL on decoding error.
*/
-NT_Bool NT_SetEntryBoolean(NT_Entry entry, uint64_t time, NT_Bool v_boolean,
- NT_Bool force);
+struct NT_Meta_TopicPublisher* NT_Meta_DecodeTopicPublishers(
+ const uint8_t* data, size_t size, size_t* count);
-/** Set Entry Double
- * Sets an entry double. If the entry name is not currently assigned to a
- * double, returns error unless the force parameter is set.
+/**
+ * Decodes `$sub$<topic>` meta-topic data.
*
- * @param entry entry handle
- * @param time timestamp
- * @param v_double double value to set
- * @param force 1 to force the entry to get overwritten, otherwise 0
- * @return 0 on error (type mismatch), 1 on success
+ * @param data data contents
+ * @param size size of data contents
+ * @param count number of elements in returned array (output)
+ * @return Array of TopicSubscribers, or NULL on decoding error.
*/
-NT_Bool NT_SetEntryDouble(NT_Entry entry, uint64_t time, double v_double,
- NT_Bool force);
+struct NT_Meta_TopicSubscriber* NT_Meta_DecodeTopicSubscribers(
+ const uint8_t* data, size_t size, size_t* count);
-/** Set Entry String
- * Sets an entry string. If the entry name is not currently assigned to a
- * string, returns error unless the force parameter is set.
+/**
+ * Decodes `$clientpub$<topic>` meta-topic data.
*
- * @param entry entry handle
- * @param time timestamp
- * @param str string to set (UTF-8 string)
- * @param str_len length of string to write in bytes
- * @param force 1 to force the entry to get overwritten, otherwise 0
- * @return 0 on error (type mismatch), 1 on success
+ * @param data data contents
+ * @param size size of data contents
+ * @param count number of elements in returned array (output)
+ * @return Array of ClientPublishers, or NULL on decoding error.
*/
-NT_Bool NT_SetEntryString(NT_Entry entry, uint64_t time, const char* str,
- size_t str_len, NT_Bool force);
+struct NT_Meta_ClientPublisher* NT_Meta_DecodeClientPublishers(
+ const uint8_t* data, size_t size, size_t* count);
-/** Set Entry Raw
- * Sets the raw value of an entry. If the entry name is not currently assigned
- * to a raw value, returns error unless the force parameter is set.
+/**
+ * Decodes `$clientsub$<topic>` meta-topic data.
*
- * @param entry entry handle
- * @param time timestamp
- * @param raw raw string to set (UTF-8 string)
- * @param raw_len length of raw string to write in bytes
- * @param force 1 to force the entry to get overwritten, otherwise 0
- * @return 0 on error (type mismatch), 1 on success
+ * @param data data contents
+ * @param size size of data contents
+ * @param count number of elements in returned array (output)
+ * @return Array of ClientSubscribers, or NULL on decoding error.
*/
-NT_Bool NT_SetEntryRaw(NT_Entry entry, uint64_t time, const char* raw,
- size_t raw_len, NT_Bool force);
+struct NT_Meta_ClientSubscriber* NT_Meta_DecodeClientSubscribers(
+ const uint8_t* data, size_t size, size_t* count);
-/** Set Entry Boolean Array
- * Sets an entry boolean array. If the entry name is not currently assigned to
- * a boolean array, returns error unless the force parameter is set.
+/**
+ * Decodes `$clients` meta-topic data.
*
- * @param entry entry handle
- * @param time timestamp
- * @param arr boolean array to write
- * @param size number of elements in the array
- * @param force 1 to force the entry to get overwritten, otherwise 0
- * @return 0 on error (type mismatch), 1 on success
+ * @param data data contents
+ * @param size size of data contents
+ * @param count number of elements in returned array (output)
+ * @return Array of Clients, or NULL on decoding error.
*/
-NT_Bool NT_SetEntryBooleanArray(NT_Entry entry, uint64_t time, const int* arr,
- size_t size, NT_Bool force);
+struct NT_Meta_Client* NT_Meta_DecodeClients(const uint8_t* data, size_t size,
+ size_t* count);
-/** Set Entry Double Array
- * Sets an entry double array. If the entry name is not currently assigned to
- * a double array, returns error unless the force parameter is set.
+/**
+ * Frees an array of NT_Meta_TopicPublisher.
*
- * @param entry entry handle
- * @param time timestamp
- * @param arr double array to write
- * @param size number of elements in the array
- * @param force 1 to force the entry to get overwritten, otherwise 0
- * @return 0 on error (type mismatch), 1 on success
+ * @param arr pointer to the array to free
+ * @param count size of the array to free
*/
-NT_Bool NT_SetEntryDoubleArray(NT_Entry entry, uint64_t time, const double* arr,
- size_t size, NT_Bool force);
+void NT_Meta_FreeTopicPublishers(struct NT_Meta_TopicPublisher* arr,
+ size_t count);
-/** Set Entry String Array
- * Sets an entry string array. If the entry name is not currently assigned to
- * a string array, returns error unless the force parameter is set.
+/**
+ * Frees an array of NT_Meta_TopicSubscriber.
*
- * @param entry entry handle
- * @param time timestamp
- * @param arr NT_String array to write
- * @param size number of elements in the array
- * @param force 1 to force the entry to get overwritten, otherwise 0
- * @return 0 on error (type mismatch), 1 on success
+ * @param arr pointer to the array to free
+ * @param count size of the array to free
*/
-NT_Bool NT_SetEntryStringArray(NT_Entry entry, uint64_t time,
- const struct NT_String* arr, size_t size,
- NT_Bool force);
+void NT_Meta_FreeTopicSubscribers(struct NT_Meta_TopicSubscriber* arr,
+ size_t count);
-/** @} */
-/** @} */
+/**
+ * Frees an array of NT_Meta_ClientPublisher.
+ *
+ * @param arr pointer to the array to free
+ * @param count size of the array to free
+ */
+void NT_Meta_FreeClientPublishers(struct NT_Meta_ClientPublisher* arr,
+ size_t count);
+
+/**
+ * Frees an array of NT_Meta_ClientSubscriber.
+ *
+ * @param arr pointer to the array to free
+ * @param count size of the array to free
+ */
+void NT_Meta_FreeClientSubscribers(struct NT_Meta_ClientSubscriber* arr,
+ size_t count);
+
+/**
+ * Frees an array of NT_Meta_Client.
+ *
+ * @param arr pointer to the array to free
+ * @param count size of the array to free
+ */
+void NT_Meta_FreeClients(struct NT_Meta_Client* arr, size_t count);
+
/** @} */
#ifdef __cplusplus
} // extern "C"
#endif
-#endif // NTCORE_NTCORE_C_H_
+#include "ntcore_c_types.h"
diff --git a/ntcore/src/main/native/include/ntcore_cpp.h b/ntcore/src/main/native/include/ntcore_cpp.h
index 2d91fe5..cbb541b 100644
--- a/ntcore/src/main/native/include/ntcore_cpp.h
+++ b/ntcore/src/main/native/include/ntcore_cpp.h
@@ -2,23 +2,34 @@
// 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.
-#ifndef NTCORE_NTCORE_CPP_H_
-#define NTCORE_NTCORE_CPP_H_
+#pragma once
#include <stdint.h>
#include <cassert>
#include <functional>
#include <memory>
+#include <optional>
+#include <span>
#include <string>
#include <string_view>
-#include <thread>
#include <utility>
+#include <variant>
#include <vector>
-#include <wpi/span.h>
-
#include "networktables/NetworkTableValue.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp_types.h"
+
+namespace wpi {
+template <typename T>
+class SmallVectorImpl;
+class json;
+} // namespace wpi
+
+namespace wpi::log {
+class DataLog;
+} // namespace wpi::log
/** NetworkTables (ntcore) namespace */
namespace nt {
@@ -31,30 +42,76 @@
* @{
*/
-/** NetworkTables Entry Information */
-struct EntryInfo {
- /** Entry handle */
- NT_Entry entry;
+/**
+ * Event notification flags.
+ *
+ * The flags are a bitmask and must be OR'ed together to indicate the
+ * combination of events desired to be received.
+ *
+ */
+struct EventFlags {
+ EventFlags() = delete;
- /** Entry name */
+ static constexpr unsigned int kNone = NT_EVENT_NONE;
+ /**
+ * Initial listener addition.
+ * Set this flag to receive immediate notification of matches to the
+ * flag criteria.
+ */
+ static constexpr unsigned int kImmediate = NT_EVENT_IMMEDIATE;
+ /** Client connected (on server, any client connected). */
+ static constexpr unsigned int kConnected = NT_EVENT_CONNECTED;
+ /** Client disconnected (on server, any client disconnected). */
+ static constexpr unsigned int kDisconnected = NT_EVENT_DISCONNECTED;
+ /** Any connection event (connect or disconnect). */
+ static constexpr unsigned int kConnection = kConnected | kDisconnected;
+ /** New topic published. */
+ static constexpr unsigned int kPublish = NT_EVENT_PUBLISH;
+ /** Topic unpublished. */
+ static constexpr unsigned int kUnpublish = NT_EVENT_UNPUBLISH;
+ /** Topic properties changed. */
+ static constexpr unsigned int kProperties = NT_EVENT_PROPERTIES;
+ /** Any topic event (publish, unpublish, or properties changed). */
+ static constexpr unsigned int kTopic = kPublish | kUnpublish | kProperties;
+ /** Topic value updated (via network). */
+ static constexpr unsigned int kValueRemote = NT_EVENT_VALUE_REMOTE;
+ /** Topic value updated (local). */
+ static constexpr unsigned int kValueLocal = NT_EVENT_VALUE_LOCAL;
+ /** Topic value updated (network or local). */
+ static constexpr unsigned int kValueAll = kValueRemote | kValueLocal;
+ /** Log message. */
+ static constexpr unsigned int kLogMessage = NT_EVENT_LOGMESSAGE;
+ /** Time synchronized with server. */
+ static constexpr unsigned int kTimeSync = NT_EVENT_TIMESYNC;
+};
+
+/** NetworkTables Topic Information */
+struct TopicInfo {
+ /** Topic handle */
+ NT_Topic topic{0};
+
+ /** Topic name */
std::string name;
- /** Entry type */
- NT_Type type;
+ /** Topic type */
+ NT_Type type{NT_UNASSIGNED};
- /** Entry flags */
- unsigned int flags;
+ /** Topic type string */
+ std::string type_str;
- /** Timestamp of last change to entry (type or value). */
- uint64_t last_change;
+ /** Topic properties JSON string */
+ std::string properties;
- friend void swap(EntryInfo& first, EntryInfo& second) {
+ /** Get topic properties as a JSON object. */
+ wpi::json GetProperties() const;
+
+ friend void swap(TopicInfo& first, TopicInfo& second) {
using std::swap;
- swap(first.entry, second.entry);
+ swap(first.topic, second.topic);
swap(first.name, second.name);
swap(first.type, second.type);
- swap(first.flags, second.flags);
- swap(first.last_change, second.last_change);
+ swap(first.type_str, second.type_str);
+ swap(first.properties, second.properties);
}
};
@@ -62,7 +119,7 @@
struct ConnectionInfo {
/**
* The remote identifier (as set on the remote node by
- * NetworkTableInstance::SetNetworkIdentity() or nt::SetNetworkIdentity()).
+ * NetworkTableInstance::StartClient4() or nt::StartClient4()).
*/
std::string remote_id;
@@ -76,7 +133,7 @@
* The last time any update was received from the remote node (same scale as
* returned by nt::Now()).
*/
- uint64_t last_update{0};
+ int64_t last_update{0};
/**
* The protocol version being used for this connection. This in protocol
@@ -94,165 +151,30 @@
}
};
-/** NetworkTables RPC Version 1 Definition Parameter */
-struct RpcParamDef {
- RpcParamDef() = default;
- RpcParamDef(std::string_view name_, std::shared_ptr<Value> def_value_)
- : name(name_), def_value(std::move(def_value_)) {}
-
- std::string name;
- std::shared_ptr<Value> def_value;
-};
-
-/** NetworkTables RPC Version 1 Definition Result */
-struct RpcResultDef {
- RpcResultDef() = default;
- RpcResultDef(std::string_view name_, NT_Type type_)
- : name(name_), type(type_) {}
-
- std::string name;
- NT_Type type;
-};
-
-/** NetworkTables RPC Version 1 Definition */
-struct RpcDefinition {
- unsigned int version;
- std::string name;
- std::vector<RpcParamDef> params;
- std::vector<RpcResultDef> results;
-};
-
-/** NetworkTables Remote Procedure Call (Server Side) */
-class RpcAnswer {
+/** NetworkTables Value Event Data */
+class ValueEventData {
public:
- RpcAnswer() = default;
- RpcAnswer(NT_Entry entry_, NT_RpcCall call_, std::string_view name_,
- std::string_view params_, ConnectionInfo conn_)
- : entry(entry_),
- call(call_),
- name(name_),
- params(params_),
- conn(std::move(conn_)) {}
+ ValueEventData() = default;
+ ValueEventData(NT_Topic topic, NT_Handle subentry, Value value)
+ : topic{topic}, subentry{subentry}, value{std::move(value)} {}
- /** Entry handle. */
- NT_Entry entry{0};
+ /** Topic handle. */
+ NT_Topic topic{0};
- /** Call handle. */
- mutable NT_RpcCall call{0};
-
- /** Entry name. */
- std::string name;
-
- /** Call raw parameters. */
- std::string params;
-
- /** Connection that called the RPC. */
- ConnectionInfo conn;
-
- /**
- * Determines if the native handle is valid.
- * @return True if the native handle is valid, false otherwise.
- */
- explicit operator bool() const { return call != 0; }
-
- /**
- * Post RPC response (return value) for a polled RPC.
- * @param result result raw data that will be provided to remote caller
- * @return True if posting the response is valid, otherwise false
- */
- bool PostResponse(std::string_view result) const;
-
- friend void swap(RpcAnswer& first, RpcAnswer& second) {
- using std::swap;
- swap(first.entry, second.entry);
- swap(first.call, second.call);
- swap(first.name, second.name);
- swap(first.params, second.params);
- swap(first.conn, second.conn);
- }
-};
-
-/** NetworkTables Entry Notification */
-class EntryNotification {
- public:
- EntryNotification() = default;
- EntryNotification(NT_EntryListener listener_, NT_Entry entry_,
- std::string_view name_, std::shared_ptr<Value> value_,
- unsigned int flags_)
- : listener(listener_),
- entry(entry_),
- name(name_),
- value(std::move(value_)),
- flags(flags_) {}
-
- /** Listener that was triggered. */
- NT_EntryListener listener{0};
-
- /** Entry handle. */
- NT_Entry entry{0};
-
- /** Entry name. */
- std::string name;
+ /** Subscriber/entry handle. */
+ NT_Handle subentry{0};
/** The new value. */
- std::shared_ptr<Value> value;
-
- /**
- * Update flags. For example, NT_NOTIFY_NEW if the key did not previously
- * exist.
- */
- unsigned int flags{0};
-
- friend void swap(EntryNotification& first, EntryNotification& second) {
- using std::swap;
- swap(first.listener, second.listener);
- swap(first.entry, second.entry);
- swap(first.name, second.name);
- swap(first.value, second.value);
- swap(first.flags, second.flags);
- }
-};
-
-/** NetworkTables Connection Notification */
-class ConnectionNotification {
- public:
- ConnectionNotification() = default;
- ConnectionNotification(NT_ConnectionListener listener_, bool connected_,
- ConnectionInfo conn_)
- : listener(listener_), connected(connected_), conn(std::move(conn_)) {}
-
- /** Listener that was triggered. */
- NT_ConnectionListener listener{0};
-
- /** True if event is due to connection being established. */
- bool connected = false;
-
- /** Connection info. */
- ConnectionInfo conn;
-
- friend void swap(ConnectionNotification& first,
- ConnectionNotification& second) {
- using std::swap;
- swap(first.listener, second.listener);
- swap(first.connected, second.connected);
- swap(first.conn, second.conn);
- }
+ Value value;
};
/** NetworkTables log message. */
class LogMessage {
public:
LogMessage() = default;
- LogMessage(NT_Logger logger_, unsigned int level_, std::string_view filename_,
- unsigned int line_, std::string_view message_)
- : logger(logger_),
- level(level_),
- filename(filename_),
- line(line_),
- message(message_) {}
-
- /** The logger that generated the message. */
- NT_Logger logger{0};
+ LogMessage(unsigned int level, std::string_view filename, unsigned int line,
+ std::string_view message)
+ : level{level}, filename{filename}, line{line}, message{message} {}
/** Log level of the message. See NT_LogLevel. */
unsigned int level{0};
@@ -265,17 +187,199 @@
/** The message. */
std::string message;
+};
- friend void swap(LogMessage& first, LogMessage& second) {
- using std::swap;
- swap(first.logger, second.logger);
- swap(first.level, second.level);
- swap(first.filename, second.filename);
- swap(first.line, second.line);
- swap(first.message, second.message);
+/** NetworkTables time sync event data. */
+class TimeSyncEventData {
+ public:
+ TimeSyncEventData() = default;
+ TimeSyncEventData(int64_t serverTimeOffset, int64_t rtt2, bool valid)
+ : serverTimeOffset{serverTimeOffset}, rtt2{rtt2}, valid{valid} {}
+
+ /**
+ * Offset between local time and server time, in microseconds. Add this value
+ * to local time to get the estimated equivalent server time.
+ */
+ int64_t serverTimeOffset;
+
+ /** Measured round trip time divided by 2, in microseconds. */
+ int64_t rtt2;
+
+ /**
+ * If serverTimeOffset and RTT are valid. An event with this set to false is
+ * sent when the client disconnects.
+ */
+ bool valid;
+};
+
+/** NetworkTables event */
+class Event {
+ public:
+ Event() = default;
+ Event(NT_Listener listener, unsigned int flags, ConnectionInfo info)
+ : listener{listener}, flags{flags}, data{std::move(info)} {}
+ Event(NT_Listener listener, unsigned int flags, TopicInfo info)
+ : listener{listener}, flags{flags}, data{std::move(info)} {}
+ Event(NT_Listener listener, unsigned int flags, ValueEventData data)
+ : listener{listener}, flags{flags}, data{std::move(data)} {}
+ Event(NT_Listener listener, unsigned int flags, LogMessage msg)
+ : listener{listener}, flags{flags}, data{std::move(msg)} {}
+ Event(NT_Listener listener, unsigned int flags, NT_Topic topic,
+ NT_Handle subentry, Value value)
+ : listener{listener},
+ flags{flags},
+ data{ValueEventData{topic, subentry, std::move(value)}} {}
+ Event(NT_Listener listener, unsigned int flags, unsigned int level,
+ std::string_view filename, unsigned int line, std::string_view message)
+ : listener{listener},
+ flags{flags},
+ data{LogMessage{level, filename, line, message}} {}
+ Event(NT_Listener listener, unsigned int flags, int64_t serverTimeOffset,
+ int64_t rtt2, bool valid)
+ : listener{listener},
+ flags{flags},
+ data{TimeSyncEventData{serverTimeOffset, rtt2, valid}} {}
+
+ /** Listener that triggered this event. */
+ NT_Listener listener{0};
+
+ /**
+ * Event flags (NT_EventFlags). Also indicates the data included with the
+ * event:
+ * - NT_EVENT_CONNECTED or NT_EVENT_DISCONNECTED: GetConnectionInfo()
+ * - NT_EVENT_PUBLISH, NT_EVENT_UNPUBLISH, or NT_EVENT_PROPERTIES:
+ * GetTopicInfo()
+ * - NT_EVENT_VALUE, NT_EVENT_VALUE_LOCAL: GetValueData()
+ * - NT_EVENT_LOGMESSAGE: GetLogMessage()
+ * - NT_EVENT_TIMESYNC: GetTimeSyncEventData()
+ */
+ unsigned int flags{0};
+
+ /**
+ * Test event flags.
+ *
+ * @param kind event flag(s) to test
+ * @return True if flags matches kind
+ */
+ bool Is(unsigned int kind) const { return (flags & kind) != 0; }
+
+ /** Event data; content depends on flags. */
+ std::variant<ConnectionInfo, TopicInfo, ValueEventData, LogMessage,
+ TimeSyncEventData>
+ data;
+
+ const ConnectionInfo* GetConnectionInfo() const {
+ return std::get_if<ConnectionInfo>(&data);
+ }
+ ConnectionInfo* GetConnectionInfo() {
+ return std::get_if<ConnectionInfo>(&data);
+ }
+
+ const TopicInfo* GetTopicInfo() const {
+ return std::get_if<TopicInfo>(&data);
+ }
+ TopicInfo* GetTopicInfo() { return std::get_if<TopicInfo>(&data); }
+
+ const ValueEventData* GetValueEventData() const {
+ return std::get_if<ValueEventData>(&data);
+ }
+ ValueEventData* GetValueEventData() {
+ return std::get_if<ValueEventData>(&data);
+ }
+
+ const LogMessage* GetLogMessage() const {
+ return std::get_if<LogMessage>(&data);
+ }
+ LogMessage* GetLogMessage() { return std::get_if<LogMessage>(&data); }
+
+ const TimeSyncEventData* GetTimeSyncEventData() const {
+ return std::get_if<TimeSyncEventData>(&data);
+ }
+ TimeSyncEventData* GetTimeSyncEventData() {
+ return std::get_if<TimeSyncEventData>(&data);
}
};
+/** NetworkTables publish/subscribe options. */
+struct PubSubOptions {
+ /**
+ * Default value of periodic.
+ */
+ static constexpr double kDefaultPeriodic = 0.1;
+
+ /**
+ * Structure size. Must be set to sizeof(PubSubOptions).
+ */
+ unsigned int structSize = sizeof(PubSubOptions);
+
+ /**
+ * Polling storage size for a subscription. Specifies the maximum number of
+ * updates NetworkTables should store between calls to the subscriber's
+ * ReadQueue() function. If zero, defaults to 1 if sendAll is false, 20 if
+ * sendAll is true.
+ */
+ unsigned int pollStorage = 0;
+
+ /**
+ * How frequently changes will be sent over the network, in seconds.
+ * NetworkTables may send more frequently than this (e.g. use a combined
+ * minimum period for all values) or apply a restricted range to this value.
+ * The default is 100 ms.
+ */
+ double periodic = kDefaultPeriodic;
+
+ /**
+ * For subscriptions, if non-zero, value updates for ReadQueue() are not
+ * queued for this publisher.
+ */
+ NT_Publisher excludePublisher = 0;
+
+ /**
+ * Send all value changes over the network.
+ */
+ bool sendAll = false;
+
+ /**
+ * For subscriptions, don't ask for value changes (only topic announcements).
+ */
+ bool topicsOnly = false;
+
+ /**
+ * Preserve duplicate value changes (rather than ignoring them).
+ */
+ bool keepDuplicates = false;
+
+ /**
+ * Perform prefix match on subscriber topic names. Is ignored/overridden by
+ * Subscribe() functions; only present in struct for the purposes of getting
+ * information about subscriptions.
+ */
+ bool prefixMatch = false;
+
+ /**
+ * For subscriptions, if remote value updates should not be queued for
+ * ReadQueue(). See also disableLocal.
+ */
+ bool disableRemote = false;
+
+ /**
+ * For subscriptions, if local value updates should not be queued for
+ * ReadQueue(). See also disableRemote.
+ */
+ bool disableLocal = false;
+
+ /**
+ * For entries, don't queue (for ReadQueue) value updates for the entry's
+ * internal publisher.
+ */
+ bool excludeSelf = false;
+};
+
+/**
+ * Default publish/subscribe options.
+ */
+constexpr PubSubOptions kDefaultPubSubOptions;
+
/**
* @defgroup ntcore_instance_func Instance Functions
* @{
@@ -297,6 +401,13 @@
NT_Inst CreateInstance();
/**
+ * Reset the internals of an instance. Every handle previously associated
+ * with this instance will no longer be valid, except for the instance
+ * handle.
+ */
+void ResetInstance(NT_Inst inst);
+
+/**
* Destroy an instance.
* The default instance cannot be destroyed.
*
@@ -329,23 +440,6 @@
NT_Entry GetEntry(NT_Inst inst, std::string_view name);
/**
- * Get Entry Handles.
- *
- * Returns an array of entry handles. The results are optionally
- * filtered by string prefix and entry type to only return a subset of all
- * entries.
- *
- * @param inst instance handle
- * @param prefix entry name required prefix; only entries whose name
- * starts with this string are returned
- * @param types bitmask of NT_Type values; 0 is treated specially
- * as a "don't care"
- * @return Array of entry handles.
- */
-std::vector<NT_Entry> GetEntries(NT_Inst inst, std::string_view prefix,
- unsigned int types);
-
-/**
* Gets the name of the specified entry.
* Returns an empty string if the handle is invalid.
*
@@ -366,10 +460,10 @@
* Gets the last time the entry was changed.
* Returns 0 if the handle is invalid.
*
- * @param entry entry handle
+ * @param subentry subscriber or entry handle
* @return Entry last change time
*/
-uint64_t GetEntryLastChange(NT_Entry entry);
+int64_t GetEntryLastChange(NT_Handle subentry);
/**
* Get Entry Value.
@@ -377,10 +471,10 @@
* Returns copy of current entry value.
* Note that one of the type options is "unassigned".
*
- * @param entry entry handle
+ * @param subentry subscriber or entry handle
* @return entry value
*/
-std::shared_ptr<Value> GetEntryValue(NT_Entry entry);
+Value GetEntryValue(NT_Handle subentry);
/**
* Set Default Entry Value
@@ -393,7 +487,7 @@
* @param value value to be set if name does not exist
* @return False on error (value not set), True on success
*/
-bool SetDefaultEntryValue(NT_Entry entry, std::shared_ptr<Value> value);
+bool SetDefaultEntryValue(NT_Entry entry, const Value& value);
/**
* Set Entry Value.
@@ -405,22 +499,7 @@
* @param value new entry value
* @return False on error (type mismatch), True on success
*/
-bool SetEntryValue(NT_Entry entry, std::shared_ptr<Value> value);
-
-/**
- * Set Entry Type and Value.
- *
- * Sets new entry value. If type of new value differs from the type of the
- * currently stored entry, the currently stored entry type is overridden
- * (generally this will generate an Entry Assignment message).
- *
- * This is NOT the preferred method to update a value; generally
- * SetEntryValue() should be used instead, with appropriate error handling.
- *
- * @param entry entry handle
- * @param value new entry value
- */
-void SetEntryTypeValue(NT_Entry entry, std::shared_ptr<Value> value);
+bool SetEntryValue(NT_Entry entry, const Value& value);
/**
* Set Entry Flags.
@@ -439,533 +518,469 @@
unsigned int GetEntryFlags(NT_Entry entry);
/**
- * Delete Entry.
+ * Read Entry Queue.
*
- * Deletes an entry. This is a new feature in version 3.0 of the protocol,
- * so this may not have an effect if any other node in the network is not
- * version 3.0 or newer.
+ * Returns new entry values since last call.
*
- * Note: GetConnections() can be used to determine the protocol version
- * of direct remote connection(s), but this is not sufficient to determine
- * if all nodes in the network are version 3.0 or newer.
- *
- * @param entry entry handle
+ * @param subentry subscriber or entry handle
+ * @return entry value array
*/
-void DeleteEntry(NT_Entry entry);
+std::vector<Value> ReadQueueValue(NT_Handle subentry);
+
+/** @} */
/**
- * Delete All Entries.
- *
- * Deletes ALL table entries. This is a new feature in version 3.0 of the
- * so this may not have an effect if any other node in the network is not
- * version 3.0 or newer.
- *
- * Note: GetConnections() can be used to determine the protocol version
- * of direct remote connection(s), but this is not sufficient to determine
- * if all nodes in the network are version 3.0 or newer.
- *
- * @param inst instance handle
+ * @defgroup ntcore_topic_func Topic Functions
+ * @{
*/
-void DeleteAllEntries(NT_Inst inst);
/**
- * Get Entry Information.
+ * Get Published Topics.
*
- * Returns an array of entry information (name, entry type,
- * and timestamp of last change to type/value). The results are optionally
- * filtered by string prefix and entry type to only return a subset of all
- * entries.
+ * Returns an array of topic handles. The results are optionally filtered by
+ * string prefix and type to only return a subset of all topics.
*
* @param inst instance handle
- * @param prefix entry name required prefix; only entries whose name
- * starts with this string are returned
- * @param types bitmask of NT_Type values; 0 is treated specially
- * as a "don't care"
- * @return Array of entry information.
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types bitmask of NT_Type values; 0 is treated specially
+ * as a "don't care"
+ * @return Array of topic handles.
*/
-std::vector<EntryInfo> GetEntryInfo(NT_Inst inst, std::string_view prefix,
+std::vector<NT_Topic> GetTopics(NT_Inst inst, std::string_view prefix,
+ unsigned int types);
+
+/**
+ * Get Published Topics.
+ *
+ * Returns an array of topic handles. The results are optionally filtered by
+ * string prefix and type to only return a subset of all topics.
+ *
+ * @param inst instance handle
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types array of type strings
+ * @return Array of topic handles.
+ */
+std::vector<NT_Topic> GetTopics(NT_Inst inst, std::string_view prefix,
+ std::span<const std::string_view> types);
+
+/**
+ * Get Topic Information about multiple topics.
+ *
+ * Returns an array of topic information (handle, name, type, and properties).
+ * The results are optionally filtered by string prefix and type to only
+ * return a subset of all topics.
+ *
+ * @param inst instance handle
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types bitmask of NT_Type values; 0 is treated specially
+ * as a "don't care"
+ * @return Array of topic information.
+ */
+std::vector<TopicInfo> GetTopicInfo(NT_Inst inst, std::string_view prefix,
unsigned int types);
/**
- * Get Entry Information.
+ * Get Topic Information about multiple topics.
*
- * Returns information about an entry (name, entry type,
- * and timestamp of last change to type/value).
+ * Returns an array of topic information (handle, name, type, and properties).
+ * The results are optionally filtered by string prefix and type to only
+ * return a subset of all topics.
*
- * @param entry entry handle
- * @return Entry information.
+ * @param inst instance handle
+ * @param prefix name required prefix; only topics whose name
+ * starts with this string are returned
+ * @param types array of type strings
+ * @return Array of topic information.
*/
-EntryInfo GetEntryInfo(NT_Entry entry);
+std::vector<TopicInfo> GetTopicInfo(NT_Inst inst, std::string_view prefix,
+ std::span<const std::string_view> types);
+
+/**
+ * Gets Topic Information.
+ *
+ * Returns information about a topic (name, type, and properties).
+ *
+ * @param topic handle
+ * @return Topic information.
+ */
+TopicInfo GetTopicInfo(NT_Topic topic);
+
+/**
+ * Gets Topic Handle.
+ *
+ * Returns topic handle.
+ *
+ * @param inst instance handle
+ * @param name topic name
+ * @return Topic handle.
+ */
+NT_Topic GetTopic(NT_Inst inst, std::string_view name);
+
+/**
+ * Gets the name of the specified topic.
+ * Returns an empty string if the handle is invalid.
+ *
+ * @param topic topic handle
+ * @return Topic name
+ */
+std::string GetTopicName(NT_Topic topic);
+
+/**
+ * Gets the type for the specified topic, or unassigned if non existent.
+ *
+ * @param topic topic handle
+ * @return Topic type
+ */
+NT_Type GetTopicType(NT_Topic topic);
+
+/**
+ * Gets the type string for the specified topic, or empty string if non
+ * existent. This may have more information than the numeric type (especially
+ * for raw values).
+ *
+ * @param topic topic handle
+ * @return Topic type string
+ */
+std::string GetTopicTypeString(NT_Topic topic);
+
+/**
+ * Sets the persistent property of a topic. If true, the stored value is
+ * persistent through server restarts.
+ *
+ * @param topic topic handle
+ * @param value True for persistent, false for not persistent.
+ */
+void SetTopicPersistent(NT_Topic topic, bool value);
+
+/**
+ * Gets the persistent property of a topic. If true, the server retains the
+ * topic even when there are no publishers.
+ *
+ * @param topic topic handle
+ * @return persistent property value
+ */
+bool GetTopicPersistent(NT_Topic topic);
+
+/**
+ * Sets the retained property of a topic.
+ *
+ * @param topic topic handle
+ * @param value new retained property value
+ */
+void SetTopicRetained(NT_Topic topic, bool value);
+
+/**
+ * Gets the retained property of a topic.
+ *
+ * @param topic topic handle
+ * @return retained property value
+ */
+bool GetTopicRetained(NT_Topic topic);
+
+/**
+ * Determine if topic exists (e.g. has at least one publisher).
+ *
+ * @param handle Topic, entry, or subscriber handle.
+ * @return True if topic exists.
+ */
+bool GetTopicExists(NT_Handle handle);
+
+/**
+ * Gets the current value of a property (as a JSON object).
+ *
+ * @param topic topic handle
+ * @param name property name
+ * @return JSON object; null object if the property does not exist.
+ */
+wpi::json GetTopicProperty(NT_Topic topic, std::string_view name);
+
+/**
+ * Sets a property value.
+ *
+ * @param topic topic handle
+ * @param name property name
+ * @param value property value
+ */
+void SetTopicProperty(NT_Topic topic, std::string_view name,
+ const wpi::json& value);
+
+/**
+ * Deletes a property. Has no effect if the property does not exist.
+ *
+ * @param topic topic handle
+ * @param name property name
+ */
+void DeleteTopicProperty(NT_Topic topic, std::string_view name);
+
+/**
+ * Gets all topic properties as a JSON object. Each key in the object
+ * is the property name, and the corresponding value is the property value.
+ *
+ * @param topic topic handle
+ * @return JSON object
+ */
+wpi::json GetTopicProperties(NT_Topic topic);
+
+/**
+ * Updates multiple topic properties. Each key in the passed-in object is
+ * the name of the property to add/update, and the corresponding value is the
+ * property value to set for that property. Null values result in deletion
+ * of the corresponding property.
+ *
+ * @param topic topic handle
+ * @param update JSON object with keys to add/update/delete
+ * @return False if update is not a JSON object
+ */
+bool SetTopicProperties(NT_Topic topic, const wpi::json& update);
+
+/**
+ * Creates a new subscriber to value changes on a topic.
+ *
+ * @param topic topic handle
+ * @param type expected type
+ * @param typeStr expected type string
+ * @param options subscription options
+ * @return Subscriber handle
+ */
+NT_Subscriber Subscribe(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+/**
+ * Stops subscriber.
+ *
+ * @param sub subscriber handle
+ */
+void Unsubscribe(NT_Subscriber sub);
+
+/**
+ * Creates a new publisher to a topic.
+ *
+ * @param topic topic handle
+ * @param type type
+ * @param typeStr type string
+ * @param options publish options
+ * @return Publisher handle
+ */
+NT_Publisher Publish(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+/**
+ * Creates a new publisher to a topic.
+ *
+ * @param topic topic handle
+ * @param type type
+ * @param typeStr type string
+ * @param properties initial properties
+ * @param options publish options
+ * @return Publisher handle
+ */
+NT_Publisher PublishEx(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const wpi::json& properties,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+/**
+ * Stops publisher.
+ *
+ * @param pubentry publisher/entry handle
+ */
+void Unpublish(NT_Handle pubentry);
+
+/**
+ * @brief Creates a new entry (subscriber and weak publisher) to a topic.
+ *
+ * @param topic topic handle
+ * @param type type
+ * @param typeStr type string
+ * @param options publish options
+ * @return Entry handle
+ */
+NT_Entry GetEntry(NT_Topic topic, NT_Type type, std::string_view typeStr,
+ const PubSubOptions& options = kDefaultPubSubOptions);
+
+/**
+ * Stops entry subscriber/publisher.
+ *
+ * @param entry entry handle
+ */
+void ReleaseEntry(NT_Entry entry);
+
+/**
+ * Stops entry/subscriber/publisher.
+ *
+ * @param pubsubentry entry/subscriber/publisher handle
+ */
+void Release(NT_Handle pubsubentry);
+
+/**
+ * Gets the topic handle from an entry/subscriber/publisher handle.
+ *
+ * @param pubsubentry entry/subscriber/publisher handle
+ * @return Topic handle
+ */
+NT_Topic GetTopicFromHandle(NT_Handle pubsubentry);
/** @} */
/**
- * @defgroup ntcore_entrylistener_func Entry Listener Functions
+ * @defgroup ntcore_advancedsub_func Advanced Subscriber Functions
* @{
*/
/**
- * Entry listener callback function.
- * Called when a key-value pair is changed.
+ * Subscribes to multiple topics based on one or more topic name prefixes. Can
+ * be used in combination with a Value Listener or ReadQueueValue() to get value
+ * changes across all matching topics.
*
- * @param entry_listener entry listener handle returned by callback creation
- * function
- * @param name entry name
- * @param value the new value
- * @param flags update flags; for example, NT_NOTIFY_NEW if the key
- * did not previously exist
+ * @param inst instance handle
+ * @param prefixes topic name prefixes
+ * @param options subscriber options
+ * @return subscriber handle
*/
-using EntryListenerCallback =
- std::function<void(NT_EntryListener entry_listener, std::string_view name,
- std::shared_ptr<Value> value, unsigned int flags)>;
+NT_MultiSubscriber SubscribeMultiple(
+ NT_Inst inst, std::span<const std::string_view> prefixes,
+ const PubSubOptions& options = kDefaultPubSubOptions);
/**
- * Add a listener for all entries starting with a certain prefix.
+ * Unsubscribes a multi-subscriber.
*
- * @param inst instance handle
- * @param prefix UTF-8 string prefix
- * @param callback listener to add
- * @param flags NotifyKind bitmask
- * @return Listener handle
+ * @param sub multi-subscriber handle
*/
-NT_EntryListener AddEntryListener(
- NT_Inst inst, std::string_view prefix,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags);
+void UnsubscribeMultiple(NT_MultiSubscriber sub);
+
+/** @} */
/**
- * Add a listener for a single entry.
- *
- * @param entry entry handle
- * @param callback listener to add
- * @param flags NotifyKind bitmask
- * @return Listener handle
+ * @defgroup ntcore_listener_func Listener Functions
+ * @{
*/
-NT_EntryListener AddEntryListener(
- NT_Entry entry,
- std::function<void(const EntryNotification& event)> callback,
- unsigned int flags);
+
+using ListenerCallback = std::function<void(const Event&)>;
/**
- * Create a entry listener poller.
+ * Creates a listener poller.
*
* A poller provides a single queue of poll events. Events linked to this
- * poller (using AddPolledEntryListener()) will be stored in the queue and
- * must be collected by calling PollEntryListener().
- * The returned handle must be destroyed with DestroyEntryListenerPoller().
+ * poller (using AddPolledListener()) will be stored in the queue and
+ * must be collected by calling ReadListenerQueue().
+ * The returned handle must be destroyed with DestroyListenerPoller().
*
* @param inst instance handle
* @return poller handle
*/
-NT_EntryListenerPoller CreateEntryListenerPoller(NT_Inst inst);
+NT_ListenerPoller CreateListenerPoller(NT_Inst inst);
/**
- * Destroy a entry listener poller. This will abort any blocked polling
+ * Destroys a listener poller. This will abort any blocked polling
* call and prevent additional events from being generated for this poller.
*
* @param poller poller handle
*/
-void DestroyEntryListenerPoller(NT_EntryListenerPoller poller);
+void DestroyListenerPoller(NT_ListenerPoller poller);
/**
- * Create a polled entry listener.
- * The caller is responsible for calling PollEntryListener() to poll.
- *
- * @param poller poller handle
- * @param prefix UTF-8 string prefix
- * @param flags NotifyKind bitmask
- * @return Listener handle
- */
-NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
- std::string_view prefix,
- unsigned int flags);
-
-/**
- * Create a polled entry listener.
- * The caller is responsible for calling PollEntryListener() to poll.
- *
- * @param poller poller handle
- * @param entry entry handle
- * @param flags NotifyKind bitmask
- * @return Listener handle
- */
-NT_EntryListener AddPolledEntryListener(NT_EntryListenerPoller poller,
- NT_Entry entry, unsigned int flags);
-
-/**
- * Get the next entry listener event. This blocks until the next event occurs.
- * This is intended to be used with AddPolledEntryListener(); entry listeners
- * created using AddEntryListener() will not be serviced through this function.
+ * Read notifications.
*
* @param poller poller handle
- * @return Information on the entry listener events. Only returns empty if an
- * error occurred (e.g. the instance was invalid or is shutting down).
+ * @return Array of events. Returns empty array if no events since last call.
*/
-std::vector<EntryNotification> PollEntryListener(NT_EntryListenerPoller poller);
+std::vector<Event> ReadListenerQueue(NT_ListenerPoller poller);
/**
- * Get the next entry listener event. This blocks until the next event occurs
- * or it times out. This is intended to be used with AddPolledEntryListener();
- * entry listeners created using AddEntryListener() will not be serviced
- * through this function.
+ * Removes a listener.
*
- * @param poller poller handle
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Information on the entry listener events. If empty is returned and
- * and timed_out is also false, an error occurred (e.g. the instance
- * was invalid or is shutting down).
+ * @param listener Listener handle to remove
*/
-std::vector<EntryNotification> PollEntryListener(NT_EntryListenerPoller poller,
- double timeout,
- bool* timed_out);
+void RemoveListener(NT_Listener listener);
/**
- * Cancel a PollEntryListener call. This wakes up a call to
- * PollEntryListener for this poller and causes it to immediately return
- * an empty array.
+ * Wait for the listener queue to be empty. This is primarily useful
+ * for deterministic testing. This blocks until either the listener
+ * queue is empty (e.g. there are no more events that need to be passed along to
+ * callbacks or poll queues) or the timeout expires.
*
- * @param poller poller handle
- */
-void CancelPollEntryListener(NT_EntryListenerPoller poller);
-
-/**
- * Remove an entry listener.
- *
- * @param entry_listener Listener handle to remove
- */
-void RemoveEntryListener(NT_EntryListener entry_listener);
-
-/**
- * Wait for the entry listener queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the entry listener
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
+ * @param handle handle
+ * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a
+ * negative value to block indefinitely
* @return False if timed out, otherwise true.
*/
-bool WaitForEntryListenerQueue(NT_Inst inst, double timeout);
-
-/** @} */
+bool WaitForListenerQueue(NT_Handle handle, double timeout);
/**
- * @defgroup ntcore_connectionlistener_func Connection Listener Functions
- * @{
- */
-
-/**
- * Connection listener callback function.
- * Called when a network connection is made or lost.
+ * Create a listener for changes to topics with names that start with any of
+ * the given prefixes. This creates a corresponding internal subscriber with the
+ * lifetime of the listener.
*
- * @param conn_listener connection listener handle returned by callback
- * creation function
- * @param connected true if event is due to connection being established
- * @param conn connection info
+ * @param inst Instance handle
+ * @param prefixes Topic name string prefixes
+ * @param mask Bitmask of NT_EventFlags values (only topic and value events will
+ * be generated)
+ * @param callback Listener function
*/
-using ConnectionListenerCallback =
- std::function<void(NT_ConnectionListener, bool, const ConnectionInfo&)>;
+NT_Listener AddListener(NT_Inst inst,
+ std::span<const std::string_view> prefixes,
+ unsigned int mask, ListenerCallback callback);
/**
- * Add a connection listener.
+ * Create a listener.
*
- * @param inst instance handle
- * @param callback listener to add
- * @param immediate_notify notify listener of all existing connections
+ * Some combinations of handle and mask have no effect:
+ * - connection and log message events are only generated on instances
+ * - topic and value events are only generated on non-instances
+ *
+ * Adding value and topic events on a topic will create a corresponding internal
+ * subscriber with the lifetime of the listener.
+ *
+ * Adding a log message listener through this function will only result in
+ * events at NT_LOG_INFO or higher; for more customized settings, use
+ * AddLogger().
+ *
+ * @param handle Instance, topic, subscriber, multi-subscriber, or entry handle
+ * @param mask Bitmask of NT_EventFlags values
+ * @param callback Listener function
+ */
+NT_Listener AddListener(NT_Handle handle, unsigned int mask,
+ ListenerCallback callback);
+
+/**
+ * Creates a polled listener. This creates a corresponding internal subscriber
+ * with the lifetime of the listener.
+ * The caller is responsible for calling ReadListenerQueue() to poll.
+ *
+ * @param poller poller handle
+ * @param prefixes array of UTF-8 string prefixes
+ * @param mask Bitmask of NT_EventFlags values (only topic and value events will
+ * be generated)
* @return Listener handle
*/
-NT_ConnectionListener AddConnectionListener(
- NT_Inst inst,
- std::function<void(const ConnectionNotification& event)> callback,
- bool immediate_notify);
+NT_Listener AddPolledListener(NT_ListenerPoller poller,
+ std::span<const std::string_view> prefixes,
+ unsigned int mask);
/**
- * Create a connection listener poller.
+ * Creates a polled listener.
+ * The caller is responsible for calling ReadListenerQueue() to poll.
*
- * A poller provides a single queue of poll events. Events linked to this
- * poller (using AddPolledConnectionListener()) will be stored in the queue and
- * must be collected by calling PollConnectionListener().
- * The returned handle must be destroyed with DestroyConnectionListenerPoller().
+ * Some combinations of handle and mask have no effect:
+ * - connection and log message events are only generated on instances
+ * - topic and value events are only generated on non-instances
*
- * @param inst instance handle
- * @return poller handle
+ * Adding value and topic events on a topic will create a corresponding internal
+ * subscriber with the lifetime of the listener.
+ *
+ * Adding a log message listener through this function will only result in
+ * events at NT_LOG_INFO or higher; for more customized settings, use
+ * AddPolledLogger().
+ *
+ * @param poller poller handle
+ * @param handle instance, topic, subscriber, multi-subscriber, or entry handle
+ * @param mask NT_EventFlags bitmask
+ * @return Listener handle
*/
-NT_ConnectionListenerPoller CreateConnectionListenerPoller(NT_Inst inst);
-
-/**
- * Destroy a connection listener poller. This will abort any blocked polling
- * call and prevent additional events from being generated for this poller.
- *
- * @param poller poller handle
- */
-void DestroyConnectionListenerPoller(NT_ConnectionListenerPoller poller);
-
-/**
- * Create a polled connection listener.
- * The caller is responsible for calling PollConnectionListener() to poll.
- *
- * @param poller poller handle
- * @param immediate_notify notify listener of all existing connections
- */
-NT_ConnectionListener AddPolledConnectionListener(
- NT_ConnectionListenerPoller poller, bool immediate_notify);
-
-/**
- * Get the next connection event. This blocks until the next connect or
- * disconnect occurs. This is intended to be used with
- * AddPolledConnectionListener(); connection listeners created using
- * AddConnectionListener() will not be serviced through this function.
- *
- * @param poller poller handle
- * @return Information on the connection events. Only returns empty if an
- * error occurred (e.g. the instance was invalid or is shutting down).
- */
-std::vector<ConnectionNotification> PollConnectionListener(
- NT_ConnectionListenerPoller poller);
-
-/**
- * Get the next connection event. This blocks until the next connect or
- * disconnect occurs or it times out. This is intended to be used with
- * AddPolledConnectionListener(); connection listeners created using
- * AddConnectionListener() will not be serviced through this function.
- *
- * @param poller poller handle
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Information on the connection events. If empty is returned and
- * timed_out is also false, an error occurred (e.g. the instance was
- * invalid or is shutting down).
- */
-std::vector<ConnectionNotification> PollConnectionListener(
- NT_ConnectionListenerPoller poller, double timeout, bool* timed_out);
-
-/**
- * Cancel a PollConnectionListener call. This wakes up a call to
- * PollConnectionListener for this poller and causes it to immediately return
- * an empty array.
- *
- * @param poller poller handle
- */
-void CancelPollConnectionListener(NT_ConnectionListenerPoller poller);
-
-/**
- * Remove a connection listener.
- *
- * @param conn_listener Listener handle to remove
- */
-void RemoveConnectionListener(NT_ConnectionListener conn_listener);
-
-/**
- * Wait for the connection listener queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the connection listener
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
-bool WaitForConnectionListenerQueue(NT_Inst inst, double timeout);
-
-/** @} */
-
-/**
- * @defgroup ntcore_rpc_func Remote Procedure Call Functions
- * @{
- */
-
-/**
- * Create a callback-based RPC entry point. Only valid to use on the server.
- * The callback function will be called when the RPC is called.
- *
- * @param entry entry handle of RPC entry
- * @param def RPC definition
- * @param callback callback function; note the callback function must call
- * PostRpcResponse() to provide a response to the call
- */
-void CreateRpc(NT_Entry entry, std::string_view def,
- std::function<void(const RpcAnswer& answer)> callback);
-
-/**
- * Create a RPC call poller. Only valid to use on the server.
- *
- * A poller provides a single queue of poll events. Events linked to this
- * poller (using CreatePolledRpc()) will be stored in the queue and must be
- * collected by calling PollRpc().
- * The returned handle must be destroyed with DestroyRpcCallPoller().
- *
- * @param inst instance handle
- * @return poller handle
- */
-NT_RpcCallPoller CreateRpcCallPoller(NT_Inst inst);
-
-/**
- * Destroy a RPC call poller. This will abort any blocked polling call and
- * prevent additional events from being generated for this poller.
- *
- * @param poller poller handle
- */
-void DestroyRpcCallPoller(NT_RpcCallPoller poller);
-
-/**
- * Create a polled RPC entry point. Only valid to use on the server.
- * The caller is responsible for calling PollRpc() to poll for servicing
- * incoming RPC calls.
- *
- * @param entry entry handle of RPC entry
- * @param def RPC definition
- * @param poller poller handle
- */
-void CreatePolledRpc(NT_Entry entry, std::string_view def,
- NT_RpcCallPoller poller);
-
-/**
- * Get the next incoming RPC call. This blocks until the next incoming RPC
- * call is received. This is intended to be used with CreatePolledRpc();
- * RPC calls created using CreateRpc() will not be serviced through this
- * function. Upon successful return, PostRpcResponse() must be called to
- * send the return value to the caller.
- *
- * @param poller poller handle
- * @return Information on the next RPC calls. Only returns empty if an error
- * occurred (e.g. the instance was invalid or is shutting down).
- */
-std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller);
-
-/**
- * Get the next incoming RPC call. This blocks until the next incoming RPC
- * call is received or it times out. This is intended to be used with
- * CreatePolledRpc(); RPC calls created using CreateRpc() will not be
- * serviced through this function. Upon successful return,
- * PostRpcResponse() must be called to send the return value to the caller.
- *
- * @param poller poller handle
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Information on the next RPC calls. If empty and timed_out is also
- * false, an error occurred (e.g. the instance was invalid or is
- * shutting down).
- */
-std::vector<RpcAnswer> PollRpc(NT_RpcCallPoller poller, double timeout,
- bool* timed_out);
-
-/**
- * Cancel a PollRpc call. This wakes up a call to PollRpc for this poller
- * and causes it to immediately return an empty array.
- *
- * @param poller poller handle
- */
-void CancelPollRpc(NT_RpcCallPoller poller);
-
-/**
- * Wait for the incoming RPC call queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the RPC call
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
-bool WaitForRpcCallQueue(NT_Inst inst, double timeout);
-
-/**
- * Post RPC response (return value) for a polled RPC.
- * The rpc and call parameters should come from the RpcAnswer returned
- * by PollRpc().
- *
- * @param entry entry handle of RPC entry (from RpcAnswer)
- * @param call RPC call handle (from RpcAnswer)
- * @param result result raw data that will be provided to remote caller
- * @return true if the response was posted, otherwise false
- */
-bool PostRpcResponse(NT_Entry entry, NT_RpcCall call, std::string_view result);
-
-/**
- * Call a RPC function. May be used on either the client or server.
- * This function is non-blocking. Either GetRpcResult() or
- * CancelRpcResult() must be called to either get or ignore the result of
- * the call.
- *
- * @param entry entry handle of RPC entry
- * @param params parameter
- * @return RPC call handle (for use with GetRpcResult() or
- * CancelRpcResult()).
- */
-NT_RpcCall CallRpc(NT_Entry entry, std::string_view params);
-
-/**
- * Get the result (return value) of a RPC call. This function blocks until
- * the result is received.
- *
- * @param entry entry handle of RPC entry
- * @param call RPC call handle returned by CallRpc()
- * @param result received result (output)
- * @return False on error, true otherwise.
- */
-bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result);
-
-/**
- * Get the result (return value) of a RPC call. This function blocks until
- * the result is received or it times out.
- *
- * @param entry entry handle of RPC entry
- * @param call RPC call handle returned by CallRpc()
- * @param result received result (output)
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return False on error or timeout, true otherwise.
- */
-bool GetRpcResult(NT_Entry entry, NT_RpcCall call, std::string* result,
- double timeout, bool* timed_out);
-
-/**
- * Ignore the result of a RPC call. This function is non-blocking.
- *
- * @param entry entry handle of RPC entry
- * @param call RPC call handle returned by CallRpc()
- */
-void CancelRpcResult(NT_Entry entry, NT_RpcCall call);
-
-/**
- * Pack a RPC version 1 definition.
- *
- * @param def RPC version 1 definition
- * @return Raw packed bytes.
- */
-std::string PackRpcDefinition(const RpcDefinition& def);
-
-/**
- * Unpack a RPC version 1 definition. This can be used for introspection or
- * validation.
- *
- * @param packed raw packed bytes
- * @param def RPC version 1 definition (output)
- * @return True if successfully unpacked, false otherwise.
- */
-bool UnpackRpcDefinition(std::string_view packed, RpcDefinition* def);
-
-/**
- * Pack RPC values as required for RPC version 1 definition messages.
- *
- * @param values array of values to pack
- * @return Raw packed bytes.
- */
-std::string PackRpcValues(wpi::span<const std::shared_ptr<Value>> values);
-
-/**
- * Unpack RPC values as required for RPC version 1 definition messages.
- *
- * @param packed raw packed bytes
- * @param types array of data types (as provided in the RPC definition)
- * @return Array of values.
- */
-std::vector<std::shared_ptr<Value>> UnpackRpcValues(
- std::string_view packed, wpi::span<const NT_Type> types);
+NT_Listener AddPolledListener(NT_ListenerPoller poller, NT_Handle handle,
+ unsigned int mask);
/** @} */
@@ -975,16 +990,6 @@
*/
/**
- * Set the network identity of this node.
- * This is the name used during the initial connection handshake, and is
- * visible through ConnectionInfo on the remote node.
- *
- * @param inst instance handle
- * @param name identity to advertise
- */
-void SetNetworkIdentity(NT_Inst inst, std::string_view name);
-
-/**
* Get the current network mode.
*
* @param inst instance handle
@@ -1013,10 +1018,12 @@
* null terminated)
* @param listen_address the address to listen on, or null to listen on any
* address. (UTF-8 string, null terminated)
- * @param port port to communicate over.
+ * @param port3 port to communicate over (NT3)
+ * @param port4 port to communicate over (NT4)
*/
void StartServer(NT_Inst inst, std::string_view persist_filename,
- const char* listen_address, unsigned int port);
+ const char* listen_address, unsigned int port3,
+ unsigned int port4);
/**
* Stops the server if it is running.
@@ -1026,41 +1033,22 @@
void StopServer(NT_Inst inst);
/**
- * Starts a client. Use SetServer to set the server name and port.
- *
- * @param inst instance handle
- */
-void StartClient(NT_Inst inst);
-
-/**
- * Starts a client using the specified server and port
- *
- * @param inst instance handle
- * @param server_name server name (UTF-8 string, null terminated)
- * @param port port to communicate over
- */
-void StartClient(NT_Inst inst, const char* server_name, unsigned int port);
-
-/**
- * Starts a client using the specified (server, port) combinations. The
- * client will attempt to connect to each server in round robin fashion.
+ * Starts a NT3 client. Use SetServer or SetServerTeam to set the server name
+ * and port.
*
* @param inst instance handle
- * @param servers array of server name and port pairs
+ * @param identity network identity to advertise (cannot be empty string)
*/
-void StartClient(
- NT_Inst inst,
- wpi::span<const std::pair<std::string_view, unsigned int>> servers);
+void StartClient3(NT_Inst inst, std::string_view identity);
/**
- * Starts a client using commonly known robot addresses for the specified
- * team.
+ * Starts a NT4 client. Use SetServer or SetServerTeam to set the server name
+ * and port.
*
- * @param inst instance handle
- * @param team team number
- * @param port port to communicate over
+ * @param inst instance handle
+ * @param identity network identity to advertise (cannot be empty string)
*/
-void StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port);
+void StartClient4(NT_Inst inst, std::string_view identity);
/**
* Stops the client if it is running.
@@ -1087,7 +1075,7 @@
*/
void SetServer(
NT_Inst inst,
- wpi::span<const std::pair<std::string_view, unsigned int>> servers);
+ std::span<const std::pair<std::string_view, unsigned int>> servers);
/**
* Sets server addresses and port for client (without restarting client).
@@ -1105,7 +1093,7 @@
* server IP address.
*
* @param inst instance handle
- * @param port server port to use in combination with IP from DS
+ * @param port server port to use in combination with IP from DS
*/
void StartDSClient(NT_Inst inst, unsigned int port);
@@ -1117,20 +1105,23 @@
void StopDSClient(NT_Inst inst);
/**
- * Set the periodic update rate.
- * Sets how frequently updates are sent to other nodes over the network.
+ * Flush local updates.
+ *
+ * Forces an immediate flush of all local changes to the client/server.
+ * This does not flush to the network.
+ *
+ * Normally this is done on a regularly scheduled interval.
*
* @param inst instance handle
- * @param interval update interval in seconds (range 0.01 to 1.0)
*/
-void SetUpdateRate(NT_Inst inst, double interval);
+void FlushLocal(NT_Inst inst);
/**
- * Flush Entries.
+ * Flush to network.
*
* Forces an immediate flush of all local entry changes to network.
- * Normally this is done on a regularly scheduled interval (see
- * SetUpdateRate()).
+ * Normally this is done on a regularly scheduled interval (set
+ * by update rates on individual publishers).
*
* Note: flushes are rate limited to avoid excessive network traffic. If
* the time between calls is too short, the flush will occur after the minimum
@@ -1157,63 +1148,18 @@
*/
bool IsConnected(NT_Inst inst);
-/** @} */
-
/**
- * @defgroup ntcore_file_func File Save/Load Functions
- * @{
- */
-
-/**
- * Save persistent values to a file. The server automatically does this,
- * but this function provides a way to save persistent values in the same
- * format to a file on either a client or a server.
+ * Get the time offset between server time and local time. Add this value to
+ * local time to get the estimated equivalent server time. In server mode, this
+ * always returns 0. In client mode, this returns the time offset only if the
+ * client and server are connected and have exchanged synchronization messages.
+ * Note the time offset may change over time as it is periodically updated; to
+ * receive updates as events, add a listener to the "time sync" event.
*
- * @param inst instance handle
- * @param filename filename
- * @return error string, or nullptr if successful
+ * @param inst instance handle
+ * @return Time offset in microseconds (optional)
*/
-const char* SavePersistent(NT_Inst inst, std::string_view filename);
-
-/**
- * Load persistent values from a file. The server automatically does this
- * at startup, but this function provides a way to restore persistent values
- * in the same format from a file at any time on either a client or a server.
- *
- * @param inst instance handle
- * @param filename filename
- * @param warn callback function for warnings
- * @return error string, or nullptr if successful
- */
-const char* LoadPersistent(
- NT_Inst inst, std::string_view filename,
- std::function<void(size_t line, const char* msg)> warn);
-
-/**
- * Save table values to a file. The file format used is identical to
- * that used for SavePersistent.
- *
- * @param inst instance handle
- * @param filename filename
- * @param prefix save only keys starting with this prefix
- * @return error string, or nullptr if successful
- */
-const char* SaveEntries(NT_Inst inst, std::string_view filename,
- std::string_view prefix);
-
-/**
- * Load table values from a file. The file format used is identical to
- * that used for SavePersistent / LoadPersistent.
- *
- * @param inst instance handle
- * @param filename filename
- * @param prefix load only keys starting with this prefix
- * @param warn callback function for warnings
- * @return error string, or nullptr if successful
- */
-const char* LoadEntries(NT_Inst inst, std::string_view filename,
- std::string_view prefix,
- std::function<void(size_t line, const char* msg)> warn);
+std::optional<int64_t> GetServerTimeOffset(NT_Inst inst);
/** @} */
@@ -1224,12 +1170,90 @@
/**
* Returns monotonic current time in 1 us increments.
- * This is the same time base used for entry and connection timestamps.
- * This function is a compatibility wrapper around wpi::Now().
+ * This is the same time base used for value and connection timestamps.
+ * This function by default simply wraps wpi::Now(), but if SetNow() is
+ * called, this function instead returns the value passed to SetNow();
+ * this can be used to reduce overhead.
*
* @return Timestamp
*/
-uint64_t Now();
+int64_t Now();
+
+/**
+ * Sets the current timestamp used for timestamping values that do not
+ * provide a timestamp (e.g. a value of 0 is passed). For consistency,
+ * it also results in Now() returning the set value. This should generally
+ * be used only if the overhead of calling wpi::Now() is a concern.
+ * If used, it should be called periodically with the value of wpi::Now().
+ *
+ * @param timestamp timestamp (1 us increments)
+ */
+void SetNow(int64_t timestamp);
+
+/**
+ * Turns a type string into a type enum value.
+ *
+ * @param typeString type string
+ * @return Type value
+ */
+NT_Type GetTypeFromString(std::string_view typeString);
+
+/**
+ * Turns a type enum value into a type string.
+ *
+ * @param type type enum
+ * @return Type string
+ */
+std::string_view GetStringFromType(NT_Type type);
+
+/** @} */
+
+/**
+ * @defgroup ntcore_data_logger_func Data Logger Functions
+ * @{
+ */
+
+/**
+ * Starts logging entry changes to a DataLog.
+ *
+ * @param inst instance handle
+ * @param log data log object; lifetime must extend until StopEntryDataLog is
+ * called or the instance is destroyed
+ * @param prefix only store entries with names that start with this prefix;
+ * the prefix is not included in the data log entry name
+ * @param logPrefix prefix to add to data log entry names
+ * @return Data logger handle
+ */
+NT_DataLogger StartEntryDataLog(NT_Inst inst, wpi::log::DataLog& log,
+ std::string_view prefix,
+ std::string_view logPrefix);
+
+/**
+ * Stops logging entry changes to a DataLog.
+ *
+ * @param logger data logger handle
+ */
+void StopEntryDataLog(NT_DataLogger logger);
+
+/**
+ * Starts logging connection changes to a DataLog.
+ *
+ * @param inst instance handle
+ * @param log data log object; lifetime must extend until StopConnectionDataLog
+ * is called or the instance is destroyed
+ * @param name data log entry name
+ * @return Data logger handle
+ */
+NT_ConnectionDataLogger StartConnectionDataLog(NT_Inst inst,
+ wpi::log::DataLog& log,
+ std::string_view name);
+
+/**
+ * Stops logging connection changes to a DataLog.
+ *
+ * @param logger data logger handle
+ */
+void StopConnectionDataLog(NT_ConnectionDataLogger logger);
/** @} */
@@ -1246,31 +1270,13 @@
* messages outside this range will be silently ignored.
*
* @param inst instance handle
- * @param func log callback function
* @param min_level minimum log level
* @param max_level maximum log level
- * @return Logger handle
+ * @param func listener callback function
+ * @return Listener handle
*/
-NT_Logger AddLogger(NT_Inst inst,
- std::function<void(const LogMessage& msg)> func,
- unsigned int min_level, unsigned int max_level);
-
-/**
- * Create a log poller. A poller provides a single queue of poll events.
- * The returned handle must be destroyed with DestroyLoggerPoller().
- *
- * @param inst instance handle
- * @return poller handle
- */
-NT_LoggerPoller CreateLoggerPoller(NT_Inst inst);
-
-/**
- * Destroy a log poller. This will abort any blocked polling call and prevent
- * additional events from being generated for this poller.
- *
- * @param poller poller handle
- */
-void DestroyLoggerPoller(NT_LoggerPoller poller);
+NT_Listener AddLogger(NT_Inst inst, unsigned int min_level,
+ unsigned int max_level, ListenerCallback func);
/**
* Set the log level for a log poller. Events will only be generated for
@@ -1282,69 +1288,125 @@
* @param max_level maximum log level
* @return Logger handle
*/
-NT_Logger AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level,
- unsigned int max_level);
-
-/**
- * Get the next log event. This blocks until the next log occurs.
- *
- * @param poller poller handle
- * @return Information on the log events. Only returns empty if an error
- * occurred (e.g. the instance was invalid or is shutting down).
- */
-std::vector<LogMessage> PollLogger(NT_LoggerPoller poller);
-
-/**
- * Get the next log event. This blocks until the next log occurs or it times
- * out.
- *
- * @param poller poller handle
- * @param timeout timeout, in seconds
- * @param timed_out true if the timeout period elapsed (output)
- * @return Information on the log events. If empty is returned and timed_out
- * is also false, an error occurred (e.g. the instance was invalid or
- * is shutting down).
- */
-std::vector<LogMessage> PollLogger(NT_LoggerPoller poller, double timeout,
- bool* timed_out);
-
-/**
- * Cancel a PollLogger call. This wakes up a call to PollLogger for this
- * poller and causes it to immediately return an empty array.
- *
- * @param poller poller handle
- */
-void CancelPollLogger(NT_LoggerPoller poller);
-
-/**
- * Remove a logger.
- *
- * @param logger Logger handle to remove
- */
-void RemoveLogger(NT_Logger logger);
-
-/**
- * Wait for the incoming log event queue to be empty. This is primarily useful
- * for deterministic testing. This blocks until either the log event
- * queue is empty (e.g. there are no more events that need to be passed along
- * to callbacks or poll queues) or the timeout expires.
- *
- * @param inst instance handle
- * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior,
- * or a negative value to block indefinitely
- * @return False if timed out, otherwise true.
- */
-bool WaitForLoggerQueue(NT_Inst inst, double timeout);
+NT_Listener AddPolledLogger(NT_ListenerPoller poller, unsigned int min_level,
+ unsigned int max_level);
/** @} */
/** @} */
-inline bool RpcAnswer::PostResponse(std::string_view result) const {
- auto ret = PostRpcResponse(entry, call, result);
- call = 0;
- return ret;
-}
+/**
+ * NetworkTables meta-topic decoding functions.
+ */
+namespace meta {
+/**
+ * @defgroup ntcore_cpp_meta_api ntcore C++ meta-topic API
+ *
+ * Meta-topic decoders for C++.
+ *
+ * @{
+ */
+
+/**
+ * Subscriber options. Different from PubSubOptions in this reflects only
+ * options that are sent over the network.
+ */
+struct SubscriberOptions {
+ double periodic = 0.1;
+ bool topicsOnly = false;
+ bool sendAll = false;
+ bool prefixMatch = false;
+ // std::string otherStr;
+};
+
+/**
+ * Topic publisher (as published via `$pub$<topic>`).
+ */
+struct TopicPublisher {
+ std::string client;
+ uint64_t pubuid = 0;
+};
+
+/**
+ * Topic subscriber (as published via `$sub$<topic>`).
+ */
+struct TopicSubscriber {
+ std::string client;
+ uint64_t subuid = 0;
+ SubscriberOptions options;
+};
+
+/**
+ * Client publisher (as published via `$clientpub$<client>` or `$serverpub`).
+ */
+struct ClientPublisher {
+ int64_t uid = -1;
+ std::string topic;
+};
+
+/**
+ * Client subscriber (as published via `$clientsub$<client>` or `$serversub`).
+ */
+struct ClientSubscriber {
+ int64_t uid = -1;
+ std::vector<std::string> topics;
+ SubscriberOptions options;
+};
+
+/**
+ * Client (as published via `$clients`).
+ */
+struct Client {
+ std::string id;
+ std::string conn;
+ uint16_t version = 0;
+};
+
+/**
+ * Decodes `$pub$<topic>` meta-topic data.
+ *
+ * @param data data contents
+ * @return Vector of TopicPublishers, or empty optional on decoding error.
+ */
+std::optional<std::vector<TopicPublisher>> DecodeTopicPublishers(
+ std::span<const uint8_t> data);
+
+/**
+ * Decodes `$sub$<topic>` meta-topic data.
+ *
+ * @param data data contents
+ * @return Vector of TopicSubscribers, or empty optional on decoding error.
+ */
+std::optional<std::vector<TopicSubscriber>> DecodeTopicSubscribers(
+ std::span<const uint8_t> data);
+
+/**
+ * Decodes `$clientpub$<topic>` meta-topic data.
+ *
+ * @param data data contents
+ * @return Vector of ClientPublishers, or empty optional on decoding error.
+ */
+std::optional<std::vector<ClientPublisher>> DecodeClientPublishers(
+ std::span<const uint8_t> data);
+
+/**
+ * Decodes `$clientsub$<topic>` meta-topic data.
+ *
+ * @param data data contents
+ * @return Vector of ClientSubscribers, or empty optional on decoding error.
+ */
+std::optional<std::vector<ClientSubscriber>> DecodeClientSubscribers(
+ std::span<const uint8_t> data);
+
+/**
+ * Decodes `$clients` meta-topic data.
+ *
+ * @param data data contents
+ * @return Vector of Clients, or empty optional on decoding error.
+ */
+std::optional<std::vector<Client>> DecodeClients(std::span<const uint8_t> data);
+
+/** @} */
+
+} // namespace meta
} // namespace nt
-
-#endif // NTCORE_NTCORE_CPP_H_
diff --git a/ntcore/src/main/native/include/ntcore_test.h b/ntcore/src/main/native/include/ntcore_test.h
index 65d1243..7e02446 100644
--- a/ntcore/src/main/native/include/ntcore_test.h
+++ b/ntcore/src/main/native/include/ntcore_test.h
@@ -2,19 +2,19 @@
// 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.
-#ifndef NTCORE_NTCORE_TEST_H_
-#define NTCORE_NTCORE_TEST_H_
+#pragma once
#include <stdint.h>
-#include <string>
-
#include "ntcore.h"
// Functions in this header are to be used only for testing
+#ifdef __cplusplus
extern "C" {
-struct NT_String* NT_GetStringForTesting(const char* string, int* struct_size);
+#endif
+
+struct NT_String* NT_GetStringForTesting(const char* str, int* struct_size);
// No need for free as one already exists in main library
struct NT_EntryInfo* NT_GetEntryInfoForTesting(const char* name,
@@ -59,28 +59,6 @@
int* struct_size);
// No need for free as one already exists in the main library
-struct NT_RpcParamDef* NT_GetRpcParamDefForTesting(const char* name,
- const struct NT_Value* val,
- int* struct_size);
-
-void NT_FreeRpcParamDefForTesting(struct NT_RpcParamDef* def);
-
-struct NT_RpcResultDef* NT_GetRpcResultsDefForTesting(const char* name,
- enum NT_Type type,
- int* struct_size);
-
-void NT_FreeRpcResultsDefForTesting(struct NT_RpcResultDef* def);
-
-struct NT_RpcDefinition* NT_GetRpcDefinitionForTesting(
- unsigned int version, const char* name, size_t num_params,
- const struct NT_RpcParamDef* params, size_t num_results,
- const struct NT_RpcResultDef* results, int* struct_size);
-// No need for free as one already exists in the main library
-
-struct NT_RpcCallInfo* NT_GetRpcCallInfoForTesting(
- unsigned int rpc_id, unsigned int call_uid, const char* name,
- const char* params, size_t params_len, int* struct_size);
-// No need for free as one already exists in the main library
+#ifdef __cplusplus
} // extern "C"
-
-#endif // NTCORE_NTCORE_TEST_H_
+#endif
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/ConnectionListenerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/ConnectionListenerTest.java
index 3c38853..9fac048 100644
--- a/ntcore/src/test/java/edu/wpi/first/networktables/ConnectionListenerTest.java
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/ConnectionListenerTest.java
@@ -11,13 +11,13 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import edu.wpi.first.util.WPIUtilJNI;
import java.util.ArrayList;
+import java.util.EnumSet;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.condition.DisabledOnOs;
-import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -28,10 +28,7 @@
@BeforeEach
void setUp() {
m_serverInst = NetworkTableInstance.create();
- m_serverInst.setNetworkIdentity("server");
-
m_clientInst = NetworkTableInstance.create();
- m_clientInst.setNetworkIdentity("client");
}
@AfterEach
@@ -42,13 +39,19 @@
/** Connect to the server. */
private void connect(int port) {
- m_serverInst.startServer("connectionlistenertest.ini", "127.0.0.1", port);
- m_clientInst.startClient("127.0.0.1", port);
+ m_serverInst.startServer("connectionlistenertest.json", "127.0.0.1", 0, port);
+ m_clientInst.startClient4("client");
+ m_clientInst.setServer("127.0.0.1", port);
- // wait for client to report it's started, then wait another 0.1 sec
+ // wait for client to report it's connected, then wait another 0.1 sec
try {
- while ((m_clientInst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) {
+ int count = 0;
+ while (!m_clientInst.isConnected()) {
Thread.sleep(100);
+ count++;
+ if (count > 30) {
+ throw new InterruptedException();
+ }
}
Thread.sleep(100);
} catch (InterruptedException ex) {
@@ -57,31 +60,31 @@
}
@Test
- @DisabledOnOs(OS.WINDOWS)
void testJNI() {
// set up the poller
- int poller = NetworkTablesJNI.createConnectionListenerPoller(m_serverInst.getHandle());
+ int poller = NetworkTablesJNI.createListenerPoller(m_serverInst.getHandle());
assertNotSame(poller, 0, "bad poller handle");
- int handle = NetworkTablesJNI.addPolledConnectionListener(poller, false);
+ int handle =
+ NetworkTablesJNI.addListener(
+ poller, m_serverInst.getHandle(), EnumSet.of(NetworkTableEvent.Kind.kConnection));
assertNotSame(handle, 0, "bad listener handle");
// trigger a connect event
connect(10020);
// get the event
- assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
- ConnectionNotification[] events = null;
try {
- events = NetworkTablesJNI.pollConnectionListenerTimeout(m_serverInst, poller, 0.0);
+ assertFalse(WPIUtilJNI.waitForObjectTimeout(handle, 1.0));
} catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- fail("unexpected interrupted exception" + ex);
+ fail("interrupted while waiting for queue");
}
+ NetworkTableEvent[] events = NetworkTablesJNI.readListenerQueue(m_serverInst, poller);
assertNotNull(events);
assertEquals(1, events.length);
assertEquals(handle, events[0].listener);
- assertTrue(events[0].connected);
+ assertNotNull(events[0].connInfo);
+ assertTrue(events[0].is(NetworkTableEvent.Kind.kConnected));
// trigger a disconnect event
m_clientInst.stopClient();
@@ -92,50 +95,71 @@
}
// get the event
- assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
try {
- events = NetworkTablesJNI.pollConnectionListenerTimeout(m_serverInst, poller, 0.0);
+ assertFalse(WPIUtilJNI.waitForObjectTimeout(handle, 1.0));
} catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- fail("unexpected interrupted exception" + ex);
+ fail("interrupted while waiting for queue");
}
+ events = NetworkTablesJNI.readListenerQueue(m_serverInst, poller);
assertNotNull(events);
assertEquals(1, events.length);
assertEquals(handle, events[0].listener);
- assertFalse(events[0].connected);
+ assertTrue(events[0].is(NetworkTableEvent.Kind.kDisconnected));
}
private static int threadedPort = 10001;
@ParameterizedTest
- @DisabledOnOs(OS.WINDOWS)
@ValueSource(strings = {"127.0.0.1", "127.0.0.1 ", " 127.0.0.1 "})
void testThreaded(String address) {
- m_serverInst.startServer("connectionlistenertest.ini", address, threadedPort);
- List<ConnectionNotification> events = new ArrayList<>();
- final int handle = m_serverInst.addConnectionListener(events::add, false);
+ m_serverInst.startServer("connectionlistenertest.json", address, 0, threadedPort);
+ List<NetworkTableEvent> events = new ArrayList<>();
+ final int handle =
+ m_serverInst.addConnectionListener(
+ false,
+ e -> {
+ synchronized (events) {
+ events.add(e);
+ }
+ });
// trigger a connect event
- m_clientInst.startClient(address, threadedPort);
+ m_clientInst.startClient4("client");
+ m_clientInst.setServer(address, threadedPort);
threadedPort++;
- // wait for client to report it's started, then wait another 0.1 sec
+ // wait for client to report it's connected, then wait another 0.1 sec
try {
- while ((m_clientInst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) {
+ int count = 0;
+ while (!m_clientInst.isConnected()) {
Thread.sleep(100);
+ count++;
+ if (count > 30) {
+ throw new InterruptedException();
+ }
}
Thread.sleep(100);
} catch (InterruptedException ex) {
fail("interrupted while waiting for client to start");
}
- assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
+ try {
+ assertFalse(WPIUtilJNI.waitForObjectTimeout(handle, 1.0));
+ } catch (InterruptedException ex) {
+ fail("interrupted while waiting for queue");
+ }
+
+ // wait for thread
+ m_serverInst.waitForListenerQueue(1.0);
// get the event
- assertEquals(1, events.size());
- assertEquals(handle, events.get(0).listener);
- assertTrue(events.get(0).connected);
- events.clear();
+ synchronized (events) {
+ assertEquals(1, events.size());
+ assertEquals(handle, events.get(0).listener);
+ assertNotNull(events.get(0).connInfo);
+ assertTrue(events.get(0).is(NetworkTableEvent.Kind.kConnected));
+ events.clear();
+ }
// trigger a disconnect event
m_clientInst.stopClient();
@@ -145,10 +169,20 @@
fail("interrupted while waiting for client to stop");
}
+ // wait for thread
+ m_serverInst.waitForListenerQueue(1.0);
+
// get the event
- assertTrue(m_serverInst.waitForConnectionListenerQueue(1.0));
- assertEquals(1, events.size());
- assertEquals(handle, events.get(0).listener);
- assertFalse(events.get(0).connected);
+ try {
+ assertFalse(WPIUtilJNI.waitForObjectTimeout(handle, 1.0));
+ } catch (InterruptedException ex) {
+ fail("interrupted while waiting for queue");
+ }
+ synchronized (events) {
+ assertEquals(1, events.size());
+ assertEquals(handle, events.get(0).listener);
+ assertNotNull(events.get(0).connInfo);
+ assertTrue(events.get(0).is(NetworkTableEvent.Kind.kDisconnected));
+ }
}
}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java
deleted file mode 100644
index 7b31c26..0000000
--- a/ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-// 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 static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-import java.util.ArrayList;
-import java.util.List;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-class EntryListenerTest {
- private NetworkTableInstance m_serverInst;
- private NetworkTableInstance m_clientInst;
-
- @BeforeEach
- void setUp() {
- m_serverInst = NetworkTableInstance.create();
- m_serverInst.setNetworkIdentity("server");
-
- m_clientInst = NetworkTableInstance.create();
- m_clientInst.setNetworkIdentity("client");
- }
-
- @AfterEach
- void tearDown() {
- m_clientInst.close();
- m_serverInst.close();
- }
-
- private void connect() {
- m_serverInst.startServer("connectionlistenertest.ini", "127.0.0.1", 10010);
- m_clientInst.startClient("127.0.0.1", 10010);
-
- // Use connection listener to ensure we've connected
- int poller = NetworkTablesJNI.createConnectionListenerPoller(m_clientInst.getHandle());
- NetworkTablesJNI.addPolledConnectionListener(poller, false);
- try {
- if (NetworkTablesJNI.pollConnectionListenerTimeout(m_clientInst, poller, 1.0).length == 0) {
- fail("client didn't connect to server");
- }
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- fail("interrupted while waiting for server connection");
- }
- }
-
- /** Test prefix with a new remote. */
- @Test
- void testPrefixNewRemote() {
- connect();
- List<EntryNotification> events = new ArrayList<>();
- final int handle = m_serverInst.addEntryListener("/foo", events::add, EntryListenerFlags.kNew);
-
- // Trigger an event
- m_clientInst.getEntry("/foo/bar").setDouble(1.0);
- m_clientInst.getEntry("/baz").setDouble(1.0);
- m_clientInst.flush();
- try {
- Thread.sleep(100);
- } catch (InterruptedException ex) {
- fail("interrupted while waiting for entries to update");
- }
-
- assertTrue(m_serverInst.waitForEntryListenerQueue(1.0));
-
- // Check the event
- assertAll(
- "Event",
- () -> assertEquals(1, events.size()),
- () -> assertEquals(handle, events.get(0).listener),
- () -> assertEquals(m_serverInst.getEntry("/foo/bar"), events.get(0).getEntry()),
- () -> assertEquals("/foo/bar", events.get(0).name),
- () -> assertEquals(NetworkTableValue.makeDouble(1.0), events.get(0).value),
- () -> assertEquals(EntryListenerFlags.kNew, events.get(0).flags));
- }
-}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java
index 1dc26ab..c562291 100644
--- a/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java
@@ -28,15 +28,21 @@
@Test
void addMessageTest() {
- List<LogMessage> msgs = new ArrayList<>();
- m_clientInst.addLogger(msgs::add, LogMessage.kInfo, 100);
+ List<NetworkTableEvent> msgs = new ArrayList<>();
+ m_clientInst.addLogger(LogMessage.kInfo, 100, msgs::add);
- m_clientInst.startClient("127.0.0.1", 10000);
+ m_clientInst.startClient4("client");
+ m_clientInst.setServer("127.0.0.1", 10000);
// wait for client to report it's started, then wait another 0.1 sec
try {
- while ((m_clientInst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) {
+ int count = 0;
+ while (!m_clientInst.getNetworkMode().contains(NetworkTableInstance.NetworkMode.kClient4)) {
Thread.sleep(100);
+ count++;
+ if (count > 30) {
+ throw new InterruptedException();
+ }
}
Thread.sleep(100);
} catch (InterruptedException ex) {
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/TableListenerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/TableListenerTest.java
new file mode 100644
index 0000000..d59e174
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/TableListenerTest.java
@@ -0,0 +1,65 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.networktables;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.EnumSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TableListenerTest {
+ private NetworkTableInstance m_inst;
+
+ @BeforeEach
+ void setUp() {
+ m_inst = NetworkTableInstance.create();
+ }
+
+ @AfterEach
+ void tearDown() {
+ m_inst.close();
+ }
+
+ private void publishTopics() {
+ m_inst.getDoubleTopic("/foo/foovalue").publish();
+ m_inst.getDoubleTopic("/foo/bar/barvalue").publish();
+ m_inst.getDoubleTopic("/baz/bazvalue").publish();
+ }
+
+ @Test
+ void testAddListener() {
+ NetworkTable table = m_inst.getTable("/foo");
+ AtomicInteger count = new AtomicInteger();
+ table.addListener(
+ EnumSet.of(NetworkTableEvent.Kind.kTopic),
+ (atable, key, event) -> {
+ count.incrementAndGet();
+ assertEquals(atable, table);
+ assertEquals(key, "foovalue");
+ });
+ publishTopics();
+ assertTrue(m_inst.waitForListenerQueue(1.0));
+ assertEquals(count.get(), 1);
+ }
+
+ @Test
+ void testAddSubTableListener() {
+ NetworkTable table = m_inst.getTable("/foo");
+ AtomicInteger count = new AtomicInteger();
+ table.addSubTableListener(
+ (atable, key, event) -> {
+ count.incrementAndGet();
+ assertEquals(atable, table);
+ assertEquals(key, "bar");
+ });
+ publishTopics();
+ assertTrue(m_inst.waitForListenerQueue(1.0));
+ assertEquals(count.get(), 1);
+ }
+}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/TimeSyncTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/TimeSyncTest.java
new file mode 100644
index 0000000..c539d20
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/TimeSyncTest.java
@@ -0,0 +1,83 @@
+// 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TimeSyncTest {
+ private NetworkTableInstance m_inst;
+
+ @BeforeEach
+ void setUp() {
+ m_inst = NetworkTableInstance.create();
+ }
+
+ @AfterEach
+ void tearDown() {
+ m_inst.close();
+ }
+
+ @Test
+ void testLocal() {
+ var offset = m_inst.getServerTimeOffset();
+ assertFalse(offset.isPresent());
+ }
+
+ @Test
+ void testServer() {
+ var poller = new NetworkTableListenerPoller(m_inst);
+ poller.addTimeSyncListener(false);
+
+ m_inst.startServer("timesynctest.json", "127.0.0.1", 0, 10030);
+ var offset = m_inst.getServerTimeOffset();
+ assertTrue(offset.isPresent());
+ assertEquals(0L, offset.getAsLong());
+
+ NetworkTableEvent[] events = poller.readQueue();
+ assertEquals(1, events.length);
+ assertNotNull(events[0].timeSyncData);
+ assertTrue(events[0].timeSyncData.valid);
+ assertEquals(0L, events[0].timeSyncData.serverTimeOffset);
+ assertEquals(0L, events[0].timeSyncData.rtt2);
+
+ m_inst.stopServer();
+ offset = m_inst.getServerTimeOffset();
+ assertFalse(offset.isPresent());
+
+ events = poller.readQueue();
+ assertEquals(1, events.length);
+ assertNotNull(events[0].timeSyncData);
+ assertFalse(events[0].timeSyncData.valid);
+ }
+
+ @Test
+ void testClient3() {
+ m_inst.startClient3("client");
+ var offset = m_inst.getServerTimeOffset();
+ assertFalse(offset.isPresent());
+
+ m_inst.stopClient();
+ offset = m_inst.getServerTimeOffset();
+ assertFalse(offset.isPresent());
+ }
+
+ @Test
+ void testClient4() {
+ m_inst.startClient4("client");
+ var offset = m_inst.getServerTimeOffset();
+ assertFalse(offset.isPresent());
+
+ m_inst.stopClient();
+ offset = m_inst.getServerTimeOffset();
+ assertFalse(offset.isPresent());
+ }
+}
diff --git a/ntcore/src/test/java/edu/wpi/first/networktables/TopicListenerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/TopicListenerTest.java
new file mode 100644
index 0000000..9f1d67f
--- /dev/null
+++ b/ntcore/src/test/java/edu/wpi/first/networktables/TopicListenerTest.java
@@ -0,0 +1,92 @@
+// 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import edu.wpi.first.util.WPIUtilJNI;
+import java.util.EnumSet;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+class TopicListenerTest {
+ private NetworkTableInstance m_serverInst;
+ private NetworkTableInstance m_clientInst;
+
+ @BeforeEach
+ void setUp() {
+ m_serverInst = NetworkTableInstance.create();
+ m_clientInst = NetworkTableInstance.create();
+ }
+
+ @AfterEach
+ void tearDown() {
+ m_clientInst.close();
+ m_serverInst.close();
+ }
+
+ private void connect() {
+ m_serverInst.startServer("topiclistenertest.json", "127.0.0.1", 0, 10010);
+ m_clientInst.startClient4("client");
+ m_clientInst.setServer("127.0.0.1", 10010);
+
+ // Use connection listener to ensure we've connected
+ int poller = NetworkTablesJNI.createListenerPoller(m_clientInst.getHandle());
+ NetworkTablesJNI.addListener(
+ poller, m_clientInst.getHandle(), EnumSet.of(NetworkTableEvent.Kind.kConnected));
+ try {
+ if (WPIUtilJNI.waitForObjectTimeout(poller, 1.0)) {
+ fail("client didn't connect to server");
+ }
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ fail("interrupted while waiting for server connection");
+ }
+ }
+
+ /** Test prefix with a new remote. */
+ @Disabled("unreliable in CI")
+ @Test
+ void testPrefixNewRemote() {
+ connect();
+ final int poller = NetworkTablesJNI.createListenerPoller(m_serverInst.getHandle());
+ final int handle =
+ NetworkTablesJNI.addListener(
+ poller, new String[] {"/foo"}, EnumSet.of(NetworkTableEvent.Kind.kPublish));
+
+ // Trigger an event
+ m_clientInst.getEntry("/foo/bar").setDouble(1.0);
+ m_clientInst.getEntry("/baz").setDouble(1.0);
+ m_clientInst.flush();
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ex) {
+ fail("interrupted while waiting for entries to update");
+ }
+
+ try {
+ if (WPIUtilJNI.waitForObjectTimeout(poller, 1.0)) {
+ fail("never got signaled");
+ }
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ fail("interrupted while waiting for signal");
+ }
+ NetworkTableEvent[] events = NetworkTablesJNI.readListenerQueue(m_serverInst, poller);
+
+ // Check the event
+ assertEquals(1, events.length);
+ assertEquals(handle, events[0].listener);
+ assertNotNull(events[0].topicInfo);
+ assertEquals(m_serverInst.getTopic("/foo/bar"), events[0].topicInfo.getTopic());
+ assertEquals("/foo/bar", events[0].topicInfo.name);
+ assertTrue(events[0].is(NetworkTableEvent.Kind.kPublish));
+ }
+}
diff --git a/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp b/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp
index 14c327c..1561277 100644
--- a/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp
+++ b/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp
@@ -5,6 +5,9 @@
#include <chrono>
#include <thread>
+#include <wpi/Synchronization.h>
+#include <wpi/mutex.h>
+
#include "TestPrinters.h"
#include "gtest/gtest.h"
#include "ntcore_cpp.h"
@@ -12,94 +15,123 @@
class ConnectionListenerTest : public ::testing::Test {
public:
ConnectionListenerTest()
- : server_inst(nt::CreateInstance()), client_inst(nt::CreateInstance()) {
- nt::SetNetworkIdentity(server_inst, "server");
- nt::SetNetworkIdentity(client_inst, "client");
- }
+ : server_inst(nt::CreateInstance()), client_inst(nt::CreateInstance()) {}
~ConnectionListenerTest() override {
nt::DestroyInstance(server_inst);
nt::DestroyInstance(client_inst);
}
- void Connect(unsigned int port);
+ void Connect(const char* address, unsigned int port3, unsigned int port4);
protected:
NT_Inst server_inst;
NT_Inst client_inst;
};
-void ConnectionListenerTest::Connect(unsigned int port) {
- nt::StartServer(server_inst, "connectionlistenertest.ini", "127.0.0.1", port);
- nt::StartClient(client_inst, "127.0.0.1", port);
+void ConnectionListenerTest::Connect(const char* address, unsigned int port3,
+ unsigned int port4) {
+ nt::StartServer(server_inst, "connectionlistenertest.ini", address, port3,
+ port4);
+ nt::StartClient4(client_inst, "client");
+ nt::SetServer(client_inst, address, port4);
- // wait for client to report it's started, then wait another 0.1 sec
- while ((nt::GetNetworkMode(client_inst) & NT_NET_MODE_STARTING) != 0) {
+ // wait for client to report it's connected, then wait another 0.1 sec
+ int count = 0;
+ while (!nt::IsConnected(client_inst)) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ if (++count > 30) {
+ FAIL() << "timed out waiting for client to start";
+ }
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
TEST_F(ConnectionListenerTest, Polled) {
// set up the poller
- NT_ConnectionListenerPoller poller =
- nt::CreateConnectionListenerPoller(server_inst);
+ NT_ListenerPoller poller = nt::CreateListenerPoller(server_inst);
ASSERT_NE(poller, 0u);
- NT_ConnectionListener handle = nt::AddPolledConnectionListener(poller, false);
+ NT_Listener handle =
+ nt::AddPolledListener(poller, server_inst, nt::EventFlags::kConnection);
ASSERT_NE(handle, 0u);
// trigger a connect event
- Connect(10000);
+ Connect("127.0.0.1", 0, 10020);
// get the event
- ASSERT_TRUE(nt::WaitForConnectionListenerQueue(server_inst, 1.0));
bool timed_out = false;
- auto result = nt::PollConnectionListener(poller, 0.1, &timed_out);
- EXPECT_FALSE(timed_out);
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timed_out));
+ ASSERT_FALSE(timed_out);
+ auto result = nt::ReadListenerQueue(poller);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(handle, result[0].listener);
- EXPECT_TRUE(result[0].connected);
+ EXPECT_TRUE(result[0].GetConnectionInfo());
+ EXPECT_EQ(result[0].flags, nt::EventFlags::kConnected);
// trigger a disconnect event
nt::StopClient(client_inst);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// get the event
- ASSERT_TRUE(nt::WaitForConnectionListenerQueue(server_inst, 1.0));
timed_out = false;
- result = nt::PollConnectionListener(poller, 0.1, &timed_out);
- EXPECT_FALSE(timed_out);
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timed_out));
+ ASSERT_FALSE(timed_out);
+ result = nt::ReadListenerQueue(poller);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(handle, result[0].listener);
- EXPECT_FALSE(result[0].connected);
-
- // trigger a disconnect event
+ EXPECT_TRUE(result[0].GetConnectionInfo());
+ EXPECT_EQ(result[0].flags, nt::EventFlags::kDisconnected);
}
-TEST_F(ConnectionListenerTest, Threaded) {
- std::vector<nt::ConnectionNotification> result;
- auto handle = nt::AddConnectionListener(
- server_inst,
- [&](const nt::ConnectionNotification& event) { result.push_back(event); },
- false);
+class ConnectionListenerVariantTest
+ : public ConnectionListenerTest,
+ public ::testing::WithParamInterface<std::pair<const char*, int>> {};
+
+TEST_P(ConnectionListenerVariantTest, Threaded) {
+ wpi::mutex m;
+ std::vector<nt::Event> result;
+ auto handle = nt::AddListener(server_inst, nt::EventFlags::kConnection,
+ [&](auto& event) {
+ std::scoped_lock lock{m};
+ result.push_back(event);
+ });
// trigger a connect event
- Connect(10001);
+ Connect(GetParam().first, 0, 20001 + GetParam().second);
- ASSERT_TRUE(nt::WaitForConnectionListenerQueue(server_inst, 1.0));
+ bool timed_out = false;
+ ASSERT_TRUE(wpi::WaitForObject(handle, 1.0, &timed_out));
+ ASSERT_FALSE(timed_out);
// get the event
- ASSERT_EQ(result.size(), 1u);
- EXPECT_EQ(handle, result[0].listener);
- EXPECT_TRUE(result[0].connected);
- result.clear();
+ {
+ std::scoped_lock lock{m};
+ ASSERT_EQ(result.size(), 1u);
+ EXPECT_EQ(handle, result[0].listener);
+ EXPECT_TRUE(result[0].GetConnectionInfo());
+ EXPECT_EQ(result[0].flags, nt::EventFlags::kConnected);
+ result.clear();
+ }
// trigger a disconnect event
nt::StopClient(client_inst);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ // wait for thread
+ nt::WaitForListenerQueue(server_inst, 1.0);
+
// get the event
- ASSERT_EQ(result.size(), 1u);
- EXPECT_EQ(handle, result[0].listener);
- EXPECT_FALSE(result[0].connected);
+ {
+ std::scoped_lock lock{m};
+ ASSERT_EQ(result.size(), 1u);
+ EXPECT_EQ(handle, result[0].listener);
+ EXPECT_TRUE(result[0].GetConnectionInfo());
+ EXPECT_EQ(result[0].flags, nt::EventFlags::kDisconnected);
+ }
}
+
+INSTANTIATE_TEST_SUITE_P(ConnectionListenerVariantTests,
+ ConnectionListenerVariantTest,
+ testing::Values(std::pair{"127.0.0.1", 0},
+ std::pair{"127.0.0.1 ", 1},
+ std::pair{" 127.0.0.1 ", 2}));
diff --git a/ntcore/src/test/native/cpp/EntryListenerTest.cpp b/ntcore/src/test/native/cpp/EntryListenerTest.cpp
deleted file mode 100644
index 8349484..0000000
--- a/ntcore/src/test/native/cpp/EntryListenerTest.cpp
+++ /dev/null
@@ -1,165 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include <chrono>
-#include <thread>
-
-#include "TestPrinters.h"
-#include "ValueMatcher.h"
-#include "gtest/gtest.h"
-#include "ntcore_cpp.h"
-
-class EntryListenerTest : public ::testing::Test {
- public:
- EntryListenerTest()
- : server_inst(nt::CreateInstance()), client_inst(nt::CreateInstance()) {
- nt::SetNetworkIdentity(server_inst, "server");
- nt::SetNetworkIdentity(client_inst, "client");
-#if 0
- nt::AddLogger(server_inst,
- [](const nt::LogMessage& msg) {
- std::fprintf(stderr, "SERVER: %s\n", msg.message.c_str());
- },
- 0, UINT_MAX);
- nt::AddLogger(client_inst,
- [](const nt::LogMessage& msg) {
- std::fprintf(stderr, "CLIENT: %s\n", msg.message.c_str());
- },
- 0, UINT_MAX);
-#endif
- }
-
- ~EntryListenerTest() override {
- nt::DestroyInstance(server_inst);
- nt::DestroyInstance(client_inst);
- }
-
- void Connect(unsigned int port);
-
- protected:
- NT_Inst server_inst;
- NT_Inst client_inst;
-};
-
-void EntryListenerTest::Connect(unsigned int port) {
- nt::StartServer(server_inst, "entrylistenertest.ini", "127.0.0.1", port);
- nt::StartClient(client_inst, "127.0.0.1", port);
-
- // Use connection listener to ensure we've connected
- NT_ConnectionListenerPoller poller =
- nt::CreateConnectionListenerPoller(server_inst);
- nt::AddPolledConnectionListener(poller, false);
- bool timed_out = false;
- if (nt::PollConnectionListener(poller, 1.0, &timed_out).empty()) {
- FAIL() << "client didn't connect to server";
- }
-}
-
-TEST_F(EntryListenerTest, EntryNewLocal) {
- std::vector<nt::EntryNotification> events;
- auto handle = nt::AddEntryListener(
- nt::GetEntry(server_inst, "/foo"),
- [&](const nt::EntryNotification& event) { events.push_back(event); },
- NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
-
- // Trigger an event
- nt::SetEntryValue(nt::GetEntry(server_inst, "/foo/bar"),
- nt::Value::MakeDouble(2.0));
- nt::SetEntryValue(nt::GetEntry(server_inst, "/foo"),
- nt::Value::MakeDouble(1.0));
-
- ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
-
- // Check the event
- ASSERT_EQ(events.size(), 1u);
- ASSERT_EQ(events[0].listener, handle);
- ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo"));
- ASSERT_EQ(events[0].name, "/foo");
- ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
- ASSERT_EQ(events[0].flags, (unsigned int)(NT_NOTIFY_NEW | NT_NOTIFY_LOCAL));
-}
-
-TEST_F(EntryListenerTest, DISABLED_EntryNewRemote) {
- Connect(10010);
- if (HasFatalFailure()) {
- return;
- }
- std::vector<nt::EntryNotification> events;
- auto handle = nt::AddEntryListener(
- nt::GetEntry(server_inst, "/foo"),
- [&](const nt::EntryNotification& event) { events.push_back(event); },
- NT_NOTIFY_NEW);
-
- // Trigger an event
- nt::SetEntryValue(nt::GetEntry(client_inst, "/foo/bar"),
- nt::Value::MakeDouble(2.0));
- nt::SetEntryValue(nt::GetEntry(client_inst, "/foo"),
- nt::Value::MakeDouble(1.0));
- nt::Flush(client_inst);
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
-
- ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
-
- // Check the event
- ASSERT_EQ(events.size(), 1u);
- ASSERT_EQ(events[0].listener, handle);
- ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo"));
- ASSERT_EQ(events[0].name, "/foo");
- ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
- ASSERT_EQ(events[0].flags, NT_NOTIFY_NEW);
-}
-
-TEST_F(EntryListenerTest, PrefixNewLocal) {
- std::vector<nt::EntryNotification> events;
- auto handle = nt::AddEntryListener(
- server_inst, "/foo",
- [&](const nt::EntryNotification& event) { events.push_back(event); },
- NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
-
- // Trigger an event
- nt::SetEntryValue(nt::GetEntry(server_inst, "/foo/bar"),
- nt::Value::MakeDouble(1.0));
- nt::SetEntryValue(nt::GetEntry(server_inst, "/baz"),
- nt::Value::MakeDouble(1.0));
-
- ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
-
- // Check the event
- ASSERT_EQ(events.size(), 1u);
- ASSERT_EQ(events[0].listener, handle);
- ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo/bar"));
- ASSERT_EQ(events[0].name, "/foo/bar");
- ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
- ASSERT_EQ(events[0].flags, (unsigned int)(NT_NOTIFY_NEW | NT_NOTIFY_LOCAL));
-}
-
-TEST_F(EntryListenerTest, DISABLED_PrefixNewRemote) {
- Connect(10011);
- if (HasFatalFailure()) {
- return;
- }
- std::vector<nt::EntryNotification> events;
- auto handle = nt::AddEntryListener(
- server_inst, "/foo",
- [&](const nt::EntryNotification& event) { events.push_back(event); },
- NT_NOTIFY_NEW);
-
- // Trigger an event
- nt::SetEntryValue(nt::GetEntry(client_inst, "/foo/bar"),
- nt::Value::MakeDouble(1.0));
- nt::SetEntryValue(nt::GetEntry(client_inst, "/baz"),
- nt::Value::MakeDouble(1.0));
- nt::Flush(client_inst);
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
-
- ASSERT_TRUE(nt::WaitForEntryListenerQueue(server_inst, 1.0));
-
- // Check the event
- ASSERT_EQ(events.size(), 1u);
- ASSERT_EQ(events[0].listener, handle);
- ASSERT_EQ(events[0].entry, nt::GetEntry(server_inst, "/foo/bar"));
- ASSERT_EQ(events[0].name, "/foo/bar");
- ASSERT_THAT(events[0].value, nt::ValueEq(nt::Value::MakeDouble(1.0)));
- ASSERT_EQ(events[0].flags, NT_NOTIFY_NEW);
-}
diff --git a/ntcore/src/test/native/cpp/EntryNotifierTest.cpp b/ntcore/src/test/native/cpp/EntryNotifierTest.cpp
deleted file mode 100644
index e781b49..0000000
--- a/ntcore/src/test/native/cpp/EntryNotifierTest.cpp
+++ /dev/null
@@ -1,312 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include <wpi/Logger.h>
-#include <wpi/StringExtras.h>
-
-#include "EntryNotifier.h"
-#include "TestPrinters.h"
-#include "ValueMatcher.h"
-#include "gtest/gtest.h"
-
-using ::testing::_;
-using ::testing::AnyNumber;
-using ::testing::IsNull;
-using ::testing::Return;
-
-namespace nt {
-
-class EntryNotifierTest : public ::testing::Test {
- public:
- EntryNotifierTest() : notifier(1, logger) { notifier.Start(); }
-
- void GenerateNotifications();
-
- protected:
- wpi::Logger logger;
- EntryNotifier notifier;
-};
-
-void EntryNotifierTest::GenerateNotifications() {
- // All flags combos that can be generated by Storage
- static const unsigned int flags[] = {
- // "normal" notifications
- NT_NOTIFY_NEW, NT_NOTIFY_DELETE, NT_NOTIFY_UPDATE, NT_NOTIFY_FLAGS,
- NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS,
- // immediate notifications are always "new"
- NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW,
- // local notifications can be of any flag combo
- NT_NOTIFY_LOCAL | NT_NOTIFY_NEW, NT_NOTIFY_LOCAL | NT_NOTIFY_DELETE,
- NT_NOTIFY_LOCAL | NT_NOTIFY_UPDATE, NT_NOTIFY_LOCAL | NT_NOTIFY_FLAGS,
- NT_NOTIFY_LOCAL | NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS};
- // Generate across keys
- static const char* keys[] = {"/foo/bar", "/baz", "/boo"};
-
- auto val = Value::MakeDouble(1);
-
- // Provide unique key indexes for each key
- unsigned int keyindex = 5;
- for (auto key : keys) {
- for (auto flag : flags) {
- notifier.NotifyEntry(keyindex, key, val, flag);
- }
- ++keyindex;
- }
-}
-
-TEST_F(EntryNotifierTest, PollEntryMultiple) {
- auto poller1 = notifier.CreatePoller();
- auto poller2 = notifier.CreatePoller();
- auto poller3 = notifier.CreatePoller();
- auto h1 = notifier.AddPolled(poller1, 6, NT_NOTIFY_NEW);
- auto h2 = notifier.AddPolled(poller2, 6, NT_NOTIFY_NEW);
- auto h3 = notifier.AddPolled(poller3, 6, NT_NOTIFY_UPDATE);
-
- ASSERT_FALSE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results1 = notifier.Poll(poller1, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- auto results2 = notifier.Poll(poller2, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- auto results3 = notifier.Poll(poller3, 0, &timed_out);
- ASSERT_FALSE(timed_out);
-
- ASSERT_EQ(results1.size(), 2u);
- for (const auto& result : results1) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h1);
- }
-
- ASSERT_EQ(results2.size(), 2u);
- for (const auto& result : results2) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h2);
- }
-
- ASSERT_EQ(results3.size(), 2u);
- for (const auto& result : results3) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h3);
- }
-}
-
-TEST_F(EntryNotifierTest, PollEntryBasic) {
- auto poller = notifier.CreatePoller();
- auto g1 = notifier.AddPolled(poller, 6, NT_NOTIFY_NEW);
- auto g2 = notifier.AddPolled(poller, 6, NT_NOTIFY_DELETE);
- auto g3 = notifier.AddPolled(poller, 6, NT_NOTIFY_UPDATE);
- auto g4 = notifier.AddPolled(poller, 6, NT_NOTIFY_FLAGS);
-
- ASSERT_FALSE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results = notifier.Poll(poller, 0, &timed_out);
- ASSERT_FALSE(timed_out);
-
- int g1count = 0;
- int g2count = 0;
- int g3count = 0;
- int g4count = 0;
- for (const auto& result : results) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_EQ(result.name, "/baz");
- EXPECT_THAT(result.value, ValueEq(Value::MakeDouble(1)));
- EXPECT_EQ(Handle{result.entry}.GetType(), Handle::kEntry);
- EXPECT_EQ(Handle{result.entry}.GetInst(), 1);
- EXPECT_EQ(Handle{result.entry}.GetIndex(), 6);
- EXPECT_EQ(Handle{result.listener}.GetType(), Handle::kEntryListener);
- EXPECT_EQ(Handle{result.listener}.GetInst(), 1);
- if (Handle{result.listener}.GetIndex() == static_cast<int>(g1)) {
- ++g1count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_NEW) != 0);
- } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g2)) {
- ++g2count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_DELETE) != 0);
- } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g3)) {
- ++g3count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_UPDATE) != 0);
- } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g4)) {
- ++g4count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_FLAGS) != 0);
- } else {
- ADD_FAILURE() << "unknown listener index";
- }
- }
- EXPECT_EQ(g1count, 2);
- EXPECT_EQ(g2count, 1); // NT_NOTIFY_DELETE
- EXPECT_EQ(g3count, 2);
- EXPECT_EQ(g4count, 2);
-}
-
-TEST_F(EntryNotifierTest, PollEntryImmediate) {
- auto poller = notifier.CreatePoller();
- notifier.AddPolled(poller, 6, NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE);
- notifier.AddPolled(poller, 6, NT_NOTIFY_NEW);
-
- ASSERT_FALSE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results = notifier.Poll(poller, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- SCOPED_TRACE(::testing::PrintToString(results));
- ASSERT_EQ(results.size(), 4u);
-}
-
-TEST_F(EntryNotifierTest, PollEntryLocal) {
- auto poller = notifier.CreatePoller();
- notifier.AddPolled(poller, 6, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
- notifier.AddPolled(poller, 6, NT_NOTIFY_NEW);
-
- ASSERT_TRUE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results = notifier.Poll(poller, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- SCOPED_TRACE(::testing::PrintToString(results));
- ASSERT_EQ(results.size(), 6u);
-}
-
-TEST_F(EntryNotifierTest, PollPrefixMultiple) {
- auto poller1 = notifier.CreatePoller();
- auto poller2 = notifier.CreatePoller();
- auto poller3 = notifier.CreatePoller();
- auto h1 = notifier.AddPolled(poller1, "/foo", NT_NOTIFY_NEW);
- auto h2 = notifier.AddPolled(poller2, "/foo", NT_NOTIFY_NEW);
- auto h3 = notifier.AddPolled(poller3, "/foo", NT_NOTIFY_UPDATE);
-
- ASSERT_FALSE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results1 = notifier.Poll(poller1, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- auto results2 = notifier.Poll(poller2, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- auto results3 = notifier.Poll(poller3, 0, &timed_out);
- ASSERT_FALSE(timed_out);
-
- ASSERT_EQ(results1.size(), 2u);
- for (const auto& result : results1) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h1);
- }
-
- ASSERT_EQ(results2.size(), 2u);
- for (const auto& result : results2) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h2);
- }
-
- ASSERT_EQ(results3.size(), 2u);
- for (const auto& result : results3) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_EQ(Handle{result.listener}.GetIndex(), (int)h3);
- }
-}
-
-TEST_F(EntryNotifierTest, PollPrefixBasic) {
- auto poller = notifier.CreatePoller();
- auto g1 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW);
- auto g2 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_DELETE);
- auto g3 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_UPDATE);
- auto g4 = notifier.AddPolled(poller, "/foo", NT_NOTIFY_FLAGS);
- notifier.AddPolled(poller, "/bar", NT_NOTIFY_NEW);
- notifier.AddPolled(poller, "/bar", NT_NOTIFY_DELETE);
- notifier.AddPolled(poller, "/bar", NT_NOTIFY_UPDATE);
- notifier.AddPolled(poller, "/bar", NT_NOTIFY_FLAGS);
-
- ASSERT_FALSE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results = notifier.Poll(poller, 0, &timed_out);
- ASSERT_FALSE(timed_out);
-
- int g1count = 0;
- int g2count = 0;
- int g3count = 0;
- int g4count = 0;
- for (const auto& result : results) {
- SCOPED_TRACE(::testing::PrintToString(result));
- EXPECT_TRUE(wpi::starts_with(result.name, "/foo"));
- EXPECT_THAT(result.value, ValueEq(Value::MakeDouble(1)));
- EXPECT_EQ(Handle{result.entry}.GetType(), Handle::kEntry);
- EXPECT_EQ(Handle{result.entry}.GetInst(), 1);
- EXPECT_EQ(Handle{result.entry}.GetIndex(), 5);
- EXPECT_EQ(Handle{result.listener}.GetType(), Handle::kEntryListener);
- EXPECT_EQ(Handle{result.listener}.GetInst(), 1);
- if (Handle{result.listener}.GetIndex() == static_cast<int>(g1)) {
- ++g1count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_NEW) != 0);
- } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g2)) {
- ++g2count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_DELETE) != 0);
- } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g3)) {
- ++g3count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_UPDATE) != 0);
- } else if (Handle{result.listener}.GetIndex() == static_cast<int>(g4)) {
- ++g4count;
- EXPECT_TRUE((result.flags & NT_NOTIFY_FLAGS) != 0);
- } else {
- ADD_FAILURE() << "unknown listener index";
- }
- }
- EXPECT_EQ(g1count, 2);
- EXPECT_EQ(g2count, 1); // NT_NOTIFY_DELETE
- EXPECT_EQ(g3count, 2);
- EXPECT_EQ(g4count, 2);
-}
-
-TEST_F(EntryNotifierTest, PollPrefixImmediate) {
- auto poller = notifier.CreatePoller();
- notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE);
- notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW);
-
- ASSERT_FALSE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results = notifier.Poll(poller, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- SCOPED_TRACE(::testing::PrintToString(results));
- ASSERT_EQ(results.size(), 4u);
-}
-
-TEST_F(EntryNotifierTest, PollPrefixLocal) {
- auto poller = notifier.CreatePoller();
- notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW | NT_NOTIFY_LOCAL);
- notifier.AddPolled(poller, "/foo", NT_NOTIFY_NEW);
-
- ASSERT_TRUE(notifier.local_notifiers());
-
- GenerateNotifications();
-
- ASSERT_TRUE(notifier.WaitForQueue(1.0));
- bool timed_out = false;
- auto results = notifier.Poll(poller, 0, &timed_out);
- ASSERT_FALSE(timed_out);
- SCOPED_TRACE(::testing::PrintToString(results));
- ASSERT_EQ(results.size(), 6u);
-}
-
-} // namespace nt
diff --git a/ntcore/src/test/native/cpp/LocalStorageTest.cpp b/ntcore/src/test/native/cpp/LocalStorageTest.cpp
new file mode 100644
index 0000000..5734284
--- /dev/null
+++ b/ntcore/src/test/native/cpp/LocalStorageTest.cpp
@@ -0,0 +1,919 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "LocalStorage.h"
+#include "MockListenerStorage.h"
+#include "MockLogger.h"
+#include "PubSubOptionsMatcher.h"
+#include "SpanMatcher.h"
+#include "TestPrinters.h"
+#include "ValueMatcher.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "net/MockNetworkInterface.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+using ::testing::_;
+using ::testing::AllOf;
+using ::testing::ElementsAre;
+using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::Property;
+using ::testing::Return;
+
+namespace nt {
+
+::testing::Matcher<const PubSubOptionsImpl&> IsPubSubOptions(
+ const PubSubOptionsImpl& good) {
+ return AllOf(
+ Field("periodic", &PubSubOptionsImpl::periodicMs, good.periodicMs),
+ Field("pollStorage", &PubSubOptionsImpl::pollStorage, good.pollStorage),
+ Field("sendAll", &PubSubOptionsImpl::sendAll, good.sendAll),
+ Field("keepDuplicates", &PubSubOptionsImpl::keepDuplicates,
+ good.keepDuplicates));
+}
+
+::testing::Matcher<const PubSubOptionsImpl&> IsDefaultPubSubOptions() {
+ static constexpr PubSubOptionsImpl kDefaultPubSubOptionsImpl;
+ return IsPubSubOptions(kDefaultPubSubOptionsImpl);
+}
+
+class LocalStorageTest : public ::testing::Test {
+ public:
+ LocalStorageTest() { storage.StartNetwork(&network); }
+
+ ::testing::StrictMock<net::MockNetworkInterface> network;
+ wpi::MockLogger logger;
+ MockListenerStorage listenerStorage;
+ LocalStorage storage{0, listenerStorage, logger};
+ NT_Topic fooTopic{storage.GetTopic("foo")};
+ NT_Topic barTopic{storage.GetTopic("bar")};
+ NT_Topic bazTopic{storage.GetTopic("baz")};
+};
+
+TEST_F(LocalStorageTest, GetTopicsUnpublished) {
+ EXPECT_TRUE(storage.GetTopics("", 0).empty());
+ EXPECT_TRUE(storage.GetTopics("", {}).empty());
+ EXPECT_TRUE(storage.GetTopicInfo("", 0).empty());
+ EXPECT_TRUE(storage.GetTopicInfo("", {}).empty());
+}
+
+TEST_F(LocalStorageTest, GetTopic2) {
+ auto foo2 = storage.GetTopic("foo");
+ EXPECT_EQ(fooTopic, foo2);
+ EXPECT_NE(fooTopic, barTopic);
+}
+
+TEST_F(LocalStorageTest, GetTopicEmptyName) {
+ EXPECT_EQ(storage.GetTopic(""), 0u);
+}
+
+TEST_F(LocalStorageTest, GetEntryEmptyName) {
+ EXPECT_EQ(storage.GetEntry(""), 0u);
+}
+
+TEST_F(LocalStorageTest, GetEntryCached) {
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"tocache"}}),
+ IsDefaultPubSubOptions()));
+
+ auto entry1 = storage.GetEntry("tocache");
+ EXPECT_EQ(entry1, storage.GetEntry("tocache"));
+}
+
+TEST_F(LocalStorageTest, GetTopicName) {
+ EXPECT_EQ(storage.GetTopicName(fooTopic), "foo");
+ EXPECT_EQ(storage.GetTopicName(barTopic), "bar");
+}
+
+TEST_F(LocalStorageTest, GetTopicInfoUnpublished) {
+ auto info = storage.GetTopicInfo(fooTopic);
+ EXPECT_EQ(info.topic, fooTopic);
+ EXPECT_EQ(info.name, "foo");
+ EXPECT_EQ(info.type, NT_UNASSIGNED);
+ EXPECT_TRUE(info.type_str.empty());
+ EXPECT_EQ(info.properties, "{}");
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_UNASSIGNED);
+ EXPECT_TRUE(storage.GetTopicTypeString(fooTopic).empty());
+ EXPECT_FALSE(storage.GetTopicExists(fooTopic));
+}
+
+TEST_F(LocalStorageTest, PublishNewNoProps) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ storage.Publish(fooTopic, NT_BOOLEAN, "boolean", wpi::json::object(), {});
+
+ auto info = storage.GetTopicInfo(fooTopic);
+ EXPECT_EQ(info.properties, "{}");
+}
+
+TEST_F(LocalStorageTest, PublishNewNoPropsNull) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ auto info = storage.GetTopicInfo(fooTopic);
+ EXPECT_EQ(info.properties, "{}");
+}
+
+TEST_F(LocalStorageTest, PublishNew) {
+ wpi::json properties = {{"persistent", true}};
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, properties,
+ IsDefaultPubSubOptions()));
+ storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {{"persistent", true}}, {});
+
+ auto info = storage.GetTopicInfo(fooTopic);
+ EXPECT_EQ(info.topic, fooTopic);
+ EXPECT_EQ(info.name, "foo");
+ EXPECT_EQ(info.type, NT_BOOLEAN);
+ EXPECT_EQ(info.type_str, "boolean");
+ EXPECT_EQ(info.properties, "{\"persistent\":true}");
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+}
+
+TEST_F(LocalStorageTest, SubscribeNoTypeLocalPubPost) {
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto sub = storage.Subscribe(fooTopic, NT_UNASSIGNED, "", {});
+
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto pub = storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ auto val = Value::MakeBoolean(true, 5);
+ EXPECT_CALL(network, SetValue(pub, val));
+ storage.SetEntryValue(pub, val);
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ auto value = storage.GetEntryValue(sub);
+ ASSERT_TRUE(value.IsBoolean());
+ EXPECT_EQ(value.GetBoolean(), true);
+ EXPECT_EQ(value.time(), 5);
+
+ auto vals = storage.ReadQueueBoolean(sub);
+ ASSERT_EQ(vals.size(), 1u);
+ EXPECT_EQ(vals[0].value, true);
+ EXPECT_EQ(vals[0].time, 5);
+
+ val = Value::MakeBoolean(false, 6);
+ EXPECT_CALL(network, SetValue(pub, val));
+ storage.SetEntryValue(pub, val);
+
+ auto vals2 = storage.ReadQueueInteger(sub); // mismatched type
+ ASSERT_TRUE(vals2.empty());
+}
+
+TEST_F(LocalStorageTest, SubscribeNoTypeLocalPubPre) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto pub = storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ auto val = Value::MakeBoolean(true, 5);
+ EXPECT_CALL(network, SetValue(pub, val));
+ storage.SetEntryValue(pub, val);
+
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto sub = storage.Subscribe(fooTopic, NT_UNASSIGNED, "", {});
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ auto value = storage.GetEntryValue(sub);
+ ASSERT_TRUE(value.IsBoolean());
+ EXPECT_EQ(value.GetBoolean(), true);
+ EXPECT_EQ(value.time(), 5);
+
+ auto vals = storage.ReadQueueValue(sub); // read queue won't get anything
+ ASSERT_TRUE(vals.empty());
+}
+
+TEST_F(LocalStorageTest, EntryNoTypeLocalSet) {
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto entry = storage.GetEntry(fooTopic, NT_UNASSIGNED, "", {});
+
+ // results in a publish and value set
+ auto val = Value::MakeBoolean(true, 5);
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ EXPECT_CALL(network, SetValue(_, val));
+ EXPECT_TRUE(storage.SetEntryValue(entry, val));
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ auto value = storage.GetEntryValue(entry);
+ ASSERT_TRUE(value.IsBoolean());
+ EXPECT_EQ(value.GetBoolean(), true);
+ EXPECT_EQ(value.time(), 5);
+
+ auto vals = storage.ReadQueueBoolean(entry);
+ ASSERT_EQ(vals.size(), 1u);
+ EXPECT_EQ(vals[0].value, true);
+ EXPECT_EQ(vals[0].time, 5);
+
+ // normal set with same type
+ val = Value::MakeBoolean(false, 6);
+ EXPECT_CALL(network, SetValue(_, val));
+ EXPECT_TRUE(storage.SetEntryValue(entry, val));
+
+ auto vals2 = storage.ReadQueueInteger(entry); // mismatched type
+ ASSERT_TRUE(vals2.empty());
+
+ // cannot change type; won't generate network message
+ EXPECT_FALSE(storage.SetEntryValue(entry, Value::MakeInteger(5, 7)));
+
+ // should not change type or generate queue items
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+
+ auto vals3 = storage.ReadQueueInteger(entry); // mismatched type
+ ASSERT_TRUE(vals3.empty());
+}
+
+TEST_F(LocalStorageTest, PubUnpubPub) {
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto sub = storage.Subscribe(fooTopic, NT_INTEGER, "int", {});
+
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ EXPECT_CALL(logger, Call(NT_LOG_INFO, _, _,
+ "local subscribe to 'foo' disabled due to type "
+ "mismatch (wanted 'int', published as 'boolean')"));
+ auto pub = storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ auto val = Value::MakeBoolean(true, 5);
+ EXPECT_CALL(network, SetValue(pub, val));
+ EXPECT_TRUE(storage.SetEntryValue(pub, val));
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ EXPECT_TRUE(storage.ReadQueueInteger(sub).empty());
+
+ EXPECT_CALL(network, Unpublish(pub, fooTopic));
+ storage.Unpublish(pub);
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_UNASSIGNED);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "");
+ EXPECT_FALSE(storage.GetTopicExists(fooTopic));
+
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"int"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ pub = storage.Publish(fooTopic, NT_INTEGER, "int", {}, {});
+
+ val = Value::MakeInteger(3, 5);
+ EXPECT_CALL(network, SetValue(pub, val));
+ EXPECT_TRUE(storage.SetEntryValue(pub, val));
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_INTEGER);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "int");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ EXPECT_EQ(storage.ReadQueueInteger(sub).size(), 1u);
+}
+
+TEST_F(LocalStorageTest, LocalPubConflict) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto pub1 = storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ EXPECT_CALL(logger, Call(NT_LOG_INFO, _, _,
+ "local publish to 'foo' disabled due to type "
+ "mismatch (wanted 'int', currently 'boolean')"));
+ auto pub2 = storage.Publish(fooTopic, NT_INTEGER, "int", {}, {});
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ EXPECT_CALL(network, SetValue(pub1, _));
+
+ EXPECT_TRUE(storage.SetEntryValue(pub1, Value::MakeBoolean(true, 5)));
+ EXPECT_FALSE(storage.SetEntryValue(pub2, Value::MakeInteger(3, 5)));
+
+ // unpublishing pub1 will publish pub2 to the network
+ EXPECT_CALL(network, Unpublish(pub1, fooTopic));
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"int"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ storage.Unpublish(pub1);
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_INTEGER);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "int");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ EXPECT_CALL(network, SetValue(pub2, _));
+
+ EXPECT_FALSE(storage.SetEntryValue(pub1, Value::MakeBoolean(true, 5)));
+ EXPECT_TRUE(storage.SetEntryValue(pub2, Value::MakeInteger(3, 5)));
+}
+
+TEST_F(LocalStorageTest, LocalSubConflict) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ EXPECT_CALL(logger, Call(NT_LOG_INFO, _, _,
+ "local subscribe to 'foo' disabled due to type "
+ "mismatch (wanted 'int', published as 'boolean')"));
+ storage.Subscribe(fooTopic, NT_INTEGER, "int", {});
+}
+
+TEST_F(LocalStorageTest, RemotePubConflict) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+
+ storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ EXPECT_CALL(logger, Call(NT_LOG_INFO, _, _,
+ "network announce of 'foo' overriding local publish "
+ "(was 'boolean', now 'int')"));
+
+ storage.NetworkAnnounce("foo", "int", wpi::json::object(), {});
+
+ // network overrides local
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_INTEGER);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "int");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+
+ storage.NetworkUnannounce("foo");
+
+ EXPECT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ EXPECT_EQ(storage.GetTopicTypeString(fooTopic), "boolean");
+ EXPECT_TRUE(storage.GetTopicExists(fooTopic));
+}
+
+TEST_F(LocalStorageTest, SubNonExist) {
+ // makes sure no warning is emitted
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ storage.Subscribe(fooTopic, NT_BOOLEAN, "boolean", {});
+}
+
+TEST_F(LocalStorageTest, SetDefaultSubscribe) {
+ // no publish, no value on wire, this is just handled locally
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto sub = storage.Subscribe(fooTopic, NT_BOOLEAN, "boolean", {});
+ EXPECT_TRUE(storage.SetDefaultEntryValue(sub, Value::MakeBoolean(true)));
+ auto val = storage.GetEntryValue(sub);
+ ASSERT_TRUE(val.IsBoolean());
+ ASSERT_TRUE(val.GetBoolean());
+ ASSERT_EQ(val.time(), 0);
+}
+
+TEST_F(LocalStorageTest, SetDefaultPublish) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto pub = storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {});
+
+ // expect a value across the wire
+ auto expectVal = Value::MakeBoolean(true, 0);
+ EXPECT_CALL(network, SetValue(pub, expectVal));
+ EXPECT_TRUE(storage.SetDefaultEntryValue(pub, Value::MakeBoolean(true)));
+
+ EXPECT_CALL(network, Subscribe(_, _, IsDefaultPubSubOptions()));
+ auto sub = storage.Subscribe(fooTopic, NT_BOOLEAN, "boolean", {});
+ auto val = storage.GetEntryValue(sub);
+ ASSERT_TRUE(val.IsBoolean());
+ ASSERT_TRUE(val.GetBoolean());
+ ASSERT_EQ(val.time(), 0);
+}
+
+TEST_F(LocalStorageTest, SetDefaultEntry) {
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto entry = storage.GetEntry(fooTopic, NT_BOOLEAN, "boolean", {});
+
+ // expect a publish and value
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto expectVal = Value::MakeBoolean(true, 0);
+ EXPECT_CALL(network, SetValue(_, expectVal));
+ EXPECT_TRUE(storage.SetDefaultEntryValue(entry, Value::MakeBoolean(true)));
+
+ auto val = storage.GetEntryValue(entry);
+ ASSERT_TRUE(val.IsBoolean());
+ ASSERT_TRUE(val.GetBoolean());
+ ASSERT_EQ(val.time(), 0);
+}
+
+TEST_F(LocalStorageTest, SetDefaultEntryUnassigned) {
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto entry = storage.GetEntry(fooTopic, NT_UNASSIGNED, "", {});
+
+ // expect a publish and value
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"boolean"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto expectVal = Value::MakeBoolean(true, 0);
+ EXPECT_CALL(network, SetValue(_, expectVal));
+ EXPECT_TRUE(storage.SetDefaultEntryValue(entry, Value::MakeBoolean(true)));
+
+ ASSERT_EQ(storage.GetTopicType(fooTopic), NT_BOOLEAN);
+ auto val = storage.GetEntryValue(entry);
+ ASSERT_TRUE(val.IsBoolean());
+ ASSERT_TRUE(val.GetBoolean());
+ ASSERT_EQ(val.time(), 0);
+}
+
+TEST_F(LocalStorageTest, SetDefaultEntryDiffType) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"string"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto pub = storage.Publish(fooTopic, NT_STRING, "string", {}, {});
+
+ EXPECT_FALSE(storage.SetDefaultEntryValue(pub, Value::MakeBoolean(true)));
+ ASSERT_EQ(storage.GetTopicType(fooTopic), NT_STRING);
+}
+
+TEST_F(LocalStorageTest, SetValueEmptyValue) {
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"string"}, wpi::json::object(),
+ IsDefaultPubSubOptions()));
+ auto pub = storage.Publish(fooTopic, NT_STRING, "string", {}, {});
+
+ EXPECT_FALSE(storage.SetEntryValue(pub, {}));
+}
+
+TEST_F(LocalStorageTest, SetValueEmptyUntypedEntry) {
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsDefaultPubSubOptions()));
+ auto entry = storage.GetEntry(fooTopic, NT_UNASSIGNED, "", {});
+ EXPECT_FALSE(storage.SetEntryValue(entry, {}));
+}
+
+TEST_F(LocalStorageTest, PublishUntyped) {
+ EXPECT_CALL(
+ logger,
+ Call(
+ NT_LOG_ERROR, _, _,
+ "cannot publish 'foo' with an unassigned type or empty type string"));
+
+ EXPECT_EQ(storage.Publish(fooTopic, NT_UNASSIGNED, "", {}, {}), 0u);
+}
+
+TEST_F(LocalStorageTest, SetValueInvalidHandle) {
+ EXPECT_FALSE(storage.SetEntryValue(0u, {}));
+}
+
+class LocalStorageDuplicatesTest : public LocalStorageTest {
+ public:
+ void SetupPubSub(bool keepPub, bool keepSub);
+ void SetValues();
+
+ NT_Publisher pub;
+ NT_Subscriber sub;
+ Value val1 = Value::MakeDouble(1.0, 10);
+ Value val2 = Value::MakeDouble(1.0, 20); // duplicate value
+ Value val3 = Value::MakeDouble(2.0, 30);
+};
+
+void LocalStorageDuplicatesTest::SetupPubSub(bool keepPub, bool keepSub) {
+ PubSubOptionsImpl pubOptions;
+ pubOptions.keepDuplicates = keepPub;
+ EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"},
+ std::string_view{"double"}, wpi::json::object(),
+ IsPubSubOptions(pubOptions)));
+ pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {},
+ {.keepDuplicates = keepPub});
+
+ PubSubOptionsImpl subOptions;
+ subOptions.pollStorage = 10;
+ subOptions.keepDuplicates = keepSub;
+ EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}),
+ IsPubSubOptions(subOptions)));
+ sub = storage.Subscribe(fooTopic, NT_DOUBLE, "double",
+ {.pollStorage = 10, .keepDuplicates = keepSub});
+}
+
+void LocalStorageDuplicatesTest::SetValues() {
+ storage.SetEntryValue(pub, val1);
+ storage.SetEntryValue(pub, val2);
+ // verify the timestamp was updated
+ EXPECT_EQ(storage.GetEntryLastChange(sub), val2.time());
+ storage.SetEntryValue(pub, val3);
+}
+
+TEST_F(LocalStorageDuplicatesTest, Defaults) {
+ SetupPubSub(false, false);
+
+ EXPECT_CALL(network, SetValue(pub, val1));
+ EXPECT_CALL(network, SetValue(pub, val3));
+ SetValues();
+
+ // verify 2nd update was dropped locally
+ auto values = storage.ReadQueueDouble(sub);
+ ASSERT_EQ(values.size(), 2u);
+ ASSERT_EQ(values[0].value, val1.GetDouble());
+ ASSERT_EQ(values[0].time, val1.time());
+ ASSERT_EQ(values[1].value, val3.GetDouble());
+ ASSERT_EQ(values[1].time, val3.time());
+}
+
+TEST_F(LocalStorageDuplicatesTest, KeepPub) {
+ SetupPubSub(true, false);
+
+ EXPECT_CALL(network, SetValue(pub, val1)).Times(2);
+ // EXPECT_CALL(network, SetValue(pub, val2));
+ EXPECT_CALL(network, SetValue(pub, val3));
+ SetValues();
+
+ // verify all 3 updates were received locally
+ auto values = storage.ReadQueueDouble(sub);
+ ASSERT_EQ(values.size(), 3u);
+}
+
+TEST_F(LocalStorageDuplicatesTest, KeepSub) {
+ SetupPubSub(false, true);
+
+ // second update should NOT go to the network
+ EXPECT_CALL(network, SetValue(pub, val1));
+ EXPECT_CALL(network, SetValue(pub, val3));
+ SetValues();
+
+ // verify all 3 updates were received locally
+ auto values = storage.ReadQueueDouble(sub);
+ ASSERT_EQ(values.size(), 3u);
+}
+
+TEST_F(LocalStorageDuplicatesTest, FromNetwork) {
+ SetupPubSub(false, false);
+
+ // incoming from the network are treated like a normal local publish
+ auto topic = storage.NetworkAnnounce("foo", "double", {{}}, 0);
+ storage.NetworkSetValue(topic, val1);
+ storage.NetworkSetValue(topic, val2);
+ // verify the timestamp was updated
+ EXPECT_EQ(storage.GetEntryLastChange(sub), val2.time());
+ storage.NetworkSetValue(topic, val3);
+
+ // verify 2nd update was dropped locally
+ auto values = storage.ReadQueueDouble(sub);
+ ASSERT_EQ(values.size(), 2u);
+ ASSERT_EQ(values[0].value, val1.GetDouble());
+ ASSERT_EQ(values[0].time, val1.time());
+ ASSERT_EQ(values[1].value, val3.GetDouble());
+ ASSERT_EQ(values[1].time, val3.time());
+}
+
+class LocalStorageNumberVariantsTest : public LocalStorageTest {
+ public:
+ void CreateSubscriber(NT_Handle* handle, std::string_view name, NT_Type type,
+ std::string_view typeStr);
+ void CreateSubscribers();
+ void CreateSubscribersArray();
+
+ NT_Subscriber sub1, sub2, sub3, sub4;
+ NT_Entry entry;
+
+ struct SubEntry {
+ SubEntry(NT_Handle subentry, NT_Type type, std::string_view name)
+ : subentry{subentry}, type{type}, name{name} {}
+ NT_Handle subentry;
+ NT_Type type;
+ std::string name;
+ };
+ std::vector<SubEntry> subentries;
+};
+
+void LocalStorageNumberVariantsTest::CreateSubscriber(
+ NT_Handle* handle, std::string_view name, NT_Type type,
+ std::string_view typeStr) {
+ *handle = storage.Subscribe(fooTopic, type, typeStr, {});
+ subentries.emplace_back(*handle, type, name);
+}
+
+void LocalStorageNumberVariantsTest::CreateSubscribers() {
+ EXPECT_CALL(logger,
+ Call(NT_LOG_INFO, _, _,
+ "local subscribe to 'foo' disabled due to type "
+ "mismatch (wanted 'boolean', published as 'double')"));
+ CreateSubscriber(&sub1, "subDouble", NT_DOUBLE, "double");
+ CreateSubscriber(&sub2, "subInteger", NT_INTEGER, "int");
+ CreateSubscriber(&sub3, "subFloat", NT_FLOAT, "float");
+ CreateSubscriber(&sub4, "subBoolean", NT_BOOLEAN, "boolean");
+ entry = storage.GetEntry("foo");
+ subentries.emplace_back(entry, NT_UNASSIGNED, "entry");
+}
+
+void LocalStorageNumberVariantsTest::CreateSubscribersArray() {
+ EXPECT_CALL(logger,
+ Call(NT_LOG_INFO, _, _,
+ "local subscribe to 'foo' disabled due to type "
+ "mismatch (wanted 'boolean[]', published as 'double[]')"));
+ CreateSubscriber(&sub1, "subDouble", NT_DOUBLE_ARRAY, "double[]");
+ CreateSubscriber(&sub2, "subInteger", NT_INTEGER_ARRAY, "int[]");
+ CreateSubscriber(&sub3, "subFloat", NT_FLOAT_ARRAY, "float[]");
+ CreateSubscriber(&sub4, "subBoolean", NT_BOOLEAN_ARRAY, "boolean[]");
+ entry = storage.GetEntry("foo");
+ subentries.emplace_back(entry, NT_UNASSIGNED, "entry");
+}
+
+TEST_F(LocalStorageNumberVariantsTest, GetEntryPubAfter) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(5);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1);
+ EXPECT_CALL(network, SetValue(_, _)).Times(1);
+ CreateSubscribers();
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50));
+ // all subscribers get the actual type and time
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ EXPECT_EQ(storage.GetEntryType(subentry.subentry), NT_DOUBLE);
+ EXPECT_EQ(storage.GetEntryLastChange(subentry.subentry), 50);
+ }
+ // for subscribers, they get a converted value or nothing on mismatch
+ EXPECT_EQ(storage.GetEntryValue(sub1), Value::MakeDouble(1.0, 50));
+ EXPECT_EQ(storage.GetEntryValue(sub2), Value::MakeInteger(1, 50));
+ EXPECT_EQ(storage.GetEntryValue(sub3), Value::MakeFloat(1.0, 50));
+ EXPECT_EQ(storage.GetEntryValue(sub4), Value{});
+ // entrys just get whatever the value is
+ EXPECT_EQ(storage.GetEntryValue(entry), Value::MakeDouble(1.0, 50));
+}
+
+TEST_F(LocalStorageNumberVariantsTest, GetEntryPubBefore) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(5);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1);
+ EXPECT_CALL(network, SetValue(_, _)).Times(1);
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ CreateSubscribers();
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50));
+ // all subscribers get the actual type and time
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ EXPECT_EQ(storage.GetEntryType(subentry.subentry), NT_DOUBLE);
+ EXPECT_EQ(storage.GetEntryLastChange(subentry.subentry), 50);
+ }
+ // for subscribers, they get a converted value or nothing on mismatch
+ EXPECT_EQ(storage.GetEntryValue(sub1), Value::MakeDouble(1.0, 50));
+ EXPECT_EQ(storage.GetEntryValue(sub2), Value::MakeInteger(1, 50));
+ EXPECT_EQ(storage.GetEntryValue(sub3), Value::MakeFloat(1.0, 50));
+ EXPECT_EQ(storage.GetEntryValue(sub4), Value{});
+ // entrys just get whatever the value is
+ EXPECT_EQ(storage.GetEntryValue(entry), Value::MakeDouble(1.0, 50));
+}
+
+template <typename T>
+::testing::Matcher<const T&> TSEq(auto value, int64_t time) {
+ return AllOf(Field("value", &T::value, value), Field("time", &T::time, time));
+}
+
+TEST_F(LocalStorageNumberVariantsTest, GetAtomic) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(5);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1);
+ EXPECT_CALL(network, SetValue(_, _)).Times(1);
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ CreateSubscribers();
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50));
+
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ EXPECT_THAT(storage.GetAtomicDouble(subentry.subentry, 0),
+ TSEq<TimestampedDouble>(1.0, 50));
+ EXPECT_THAT(storage.GetAtomicInteger(subentry.subentry, 0),
+ TSEq<TimestampedInteger>(1, 50));
+ EXPECT_THAT(storage.GetAtomicFloat(subentry.subentry, 0),
+ TSEq<TimestampedFloat>(1.0, 50));
+ EXPECT_THAT(storage.GetAtomicBoolean(subentry.subentry, false),
+ TSEq<TimestampedBoolean>(false, 0));
+ }
+}
+
+template <typename T, typename U>
+::testing::Matcher<const T&> TSSpanEq(std::span<U> value, int64_t time) {
+ return AllOf(
+ Field("value", &T::value, wpi::SpanEq(std::span<const U>(value))),
+ Field("time", &T::time, time));
+}
+
+TEST_F(LocalStorageNumberVariantsTest, GetAtomicArray) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(5);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1);
+ EXPECT_CALL(network, SetValue(_, _)).Times(1);
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE_ARRAY, "double[]", {}, {});
+ CreateSubscribersArray();
+ storage.SetEntryValue(pub, Value::MakeDoubleArray({1.0}, 50));
+
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ double doubleVal = 1.0;
+ EXPECT_THAT(storage.GetAtomicDoubleArray(subentry.subentry, {}),
+ TSSpanEq<TimestampedDoubleArray>(std::span{&doubleVal, 1}, 50));
+ int64_t intVal = 1;
+ EXPECT_THAT(storage.GetAtomicIntegerArray(subentry.subentry, {}),
+ TSSpanEq<TimestampedIntegerArray>(std::span{&intVal, 1}, 50));
+ float floatVal = 1.0;
+ EXPECT_THAT(storage.GetAtomicFloatArray(subentry.subentry, {}),
+ TSSpanEq<TimestampedFloatArray>(std::span{&floatVal, 1}, 50));
+ EXPECT_THAT(storage.GetAtomicBooleanArray(subentry.subentry, {}),
+ TSSpanEq<TimestampedBooleanArray>(std::span<int>{}, 0));
+ }
+}
+
+TEST_F(LocalStorageNumberVariantsTest, ReadQueue) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(5);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1);
+ EXPECT_CALL(network, SetValue(_, _)).Times(4);
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ CreateSubscribers();
+
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50));
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ if (subentry.type == NT_BOOLEAN) {
+ EXPECT_THAT(storage.ReadQueueDouble(subentry.subentry), IsEmpty());
+ } else {
+ EXPECT_THAT(storage.ReadQueueDouble(subentry.subentry),
+ ElementsAre(TSEq<TimestampedDouble>(1.0, 50)));
+ }
+ }
+
+ storage.SetEntryValue(pub, Value::MakeDouble(2.0, 50));
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ if (subentry.type == NT_BOOLEAN) {
+ EXPECT_THAT(storage.ReadQueueInteger(subentry.subentry), IsEmpty());
+ } else {
+ EXPECT_THAT(storage.ReadQueueInteger(subentry.subentry),
+ ElementsAre(TSEq<TimestampedInteger>(2, 50)));
+ }
+ }
+
+ storage.SetEntryValue(pub, Value::MakeDouble(3.0, 50));
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ if (subentry.type == NT_BOOLEAN) {
+ EXPECT_THAT(storage.ReadQueueFloat(subentry.subentry), IsEmpty());
+ } else {
+ EXPECT_THAT(storage.ReadQueueFloat(subentry.subentry),
+ ElementsAre(TSEq<TimestampedFloat>(3.0, 50)));
+ }
+ }
+
+ storage.SetEntryValue(pub, Value::MakeDouble(4.0, 50));
+ for (auto&& subentry : subentries) {
+ SCOPED_TRACE(subentry.name);
+ EXPECT_THAT(storage.ReadQueueBoolean(subentry.subentry), IsEmpty());
+ }
+}
+
+TEST_F(LocalStorageTest, MultiSubSpecial) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(2);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(2);
+ EXPECT_CALL(network, SetValue(_, _)).Times(2);
+ EXPECT_CALL(listenerStorage, Activate(_, _, _)).Times(2);
+
+ auto subnormal = storage.SubscribeMultiple({{""}}, {});
+ auto subspecial = storage.SubscribeMultiple({{"", "$"}}, {});
+ auto pubnormal = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ auto specialTopic = storage.GetTopic("$topic");
+ auto pubspecial = storage.Publish(specialTopic, NT_DOUBLE, "double", {}, {});
+ storage.AddListener(1, subnormal, NT_EVENT_VALUE_ALL);
+ storage.AddListener(2, subspecial, NT_EVENT_VALUE_ALL);
+
+ EXPECT_CALL(
+ listenerStorage,
+ Notify(wpi::SpanEq(std::span<const NT_Listener>{{2}}), _, _, _, _));
+ storage.SetEntryValue(pubspecial, Value::MakeDouble(1.0, 30));
+
+ EXPECT_CALL(
+ listenerStorage,
+ Notify(wpi::SpanEq(std::span<const NT_Listener>{{1}}), _, _, _, _));
+ EXPECT_CALL(
+ listenerStorage,
+ Notify(wpi::SpanEq(std::span<const NT_Listener>{{2}}), _, _, _, _));
+ storage.SetEntryValue(pubnormal, Value::MakeDouble(2.0, 40));
+}
+
+TEST_F(LocalStorageTest, NetworkDuplicateDetect) {
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _));
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ auto remoteTopic =
+ storage.NetworkAnnounce("foo", "double", wpi::json::object(), 0);
+
+ // local set
+ EXPECT_CALL(network, SetValue(_, _));
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50));
+ // 2nd local set with same value - no SetValue call to network
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 60));
+ // network set with different value
+ storage.NetworkSetValue(remoteTopic, Value::MakeDouble(2.0, 70));
+ // 3rd local set with same value generates a SetValue call to network
+ EXPECT_CALL(network, SetValue(_, _));
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 80));
+}
+
+TEST_F(LocalStorageTest, ReadQueueLocalRemote) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(3);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1);
+
+ auto subBoth =
+ storage.Subscribe(fooTopic, NT_DOUBLE, "double", kDefaultPubSubOptions);
+ auto subLocal =
+ storage.Subscribe(fooTopic, NT_DOUBLE, "double", {.disableRemote = true});
+ auto subRemote =
+ storage.Subscribe(fooTopic, NT_DOUBLE, "double", {.disableLocal = true});
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ auto remoteTopic =
+ storage.NetworkAnnounce("foo", "double", wpi::json::object(), 0);
+
+ // local set
+ EXPECT_CALL(network, SetValue(_, _));
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50));
+ EXPECT_THAT(storage.ReadQueueDouble(subBoth),
+ ElementsAre(TSEq<TimestampedDouble>(1.0, 50)));
+ EXPECT_THAT(storage.ReadQueueDouble(subLocal),
+ ElementsAre(TSEq<TimestampedDouble>(1.0, 50)));
+ EXPECT_THAT(storage.ReadQueueDouble(subRemote), IsEmpty());
+
+ // network set
+ storage.NetworkSetValue(remoteTopic, Value::MakeDouble(2.0, 60));
+ EXPECT_THAT(storage.ReadQueueDouble(subBoth),
+ ElementsAre(TSEq<TimestampedDouble>(2.0, 60)));
+ EXPECT_THAT(storage.ReadQueueDouble(subRemote),
+ ElementsAre(TSEq<TimestampedDouble>(2.0, 60)));
+ EXPECT_THAT(storage.ReadQueueDouble(subLocal), IsEmpty());
+}
+
+TEST_F(LocalStorageTest, SubExcludePub) {
+ EXPECT_CALL(network, Subscribe(_, _, _)).Times(2);
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1);
+
+ auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {});
+ auto subActive = storage.Subscribe(fooTopic, NT_DOUBLE, "double", {});
+ auto subExclude = storage.Subscribe(fooTopic, NT_DOUBLE, "double",
+ {.excludePublisher = pub});
+ auto remoteTopic =
+ storage.NetworkAnnounce("foo", "double", wpi::json::object(), 0);
+
+ // local set
+ EXPECT_CALL(network, SetValue(_, _));
+ storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50));
+ EXPECT_THAT(storage.ReadQueueDouble(subActive),
+ ElementsAre(TSEq<TimestampedDouble>(1.0, 50)));
+ EXPECT_THAT(storage.ReadQueueDouble(subExclude), IsEmpty());
+
+ // network set
+ storage.NetworkSetValue(remoteTopic, Value::MakeDouble(2.0, 60));
+ EXPECT_THAT(storage.ReadQueueDouble(subActive),
+ ElementsAre(TSEq<TimestampedDouble>(2.0, 60)));
+ EXPECT_THAT(storage.ReadQueueDouble(subExclude),
+ ElementsAre(TSEq<TimestampedDouble>(2.0, 60)));
+}
+
+TEST_F(LocalStorageTest, EntryExcludeSelf) {
+ EXPECT_CALL(network, Subscribe(_, _, _));
+
+ auto entry =
+ storage.GetEntry(fooTopic, NT_DOUBLE, "double", {.excludeSelf = true});
+ auto remoteTopic =
+ storage.NetworkAnnounce("foo", "double", wpi::json::object(), 0);
+
+ // local set
+ EXPECT_CALL(network, Publish(_, _, _, _, _, _));
+ EXPECT_CALL(network, SetValue(_, _));
+ storage.SetEntryValue(entry, Value::MakeDouble(1.0, 50));
+ EXPECT_THAT(storage.ReadQueueDouble(entry), IsEmpty());
+
+ // network set
+ storage.NetworkSetValue(remoteTopic, Value::MakeDouble(2.0, 60));
+ EXPECT_THAT(storage.ReadQueueDouble(entry),
+ ElementsAre(TSEq<TimestampedDouble>(2.0, 60)));
+}
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/LoggerTest.cpp b/ntcore/src/test/native/cpp/LoggerTest.cpp
new file mode 100644
index 0000000..a9f499c
--- /dev/null
+++ b/ntcore/src/test/native/cpp/LoggerTest.cpp
@@ -0,0 +1,96 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <wpi/Synchronization.h>
+
+#include "Handle.h"
+#include "TestPrinters.h"
+#include "gtest/gtest.h"
+#include "ntcore_cpp.h"
+
+class LoggerTest : public ::testing::Test {
+ public:
+ LoggerTest() : m_inst(nt::CreateInstance()) {}
+
+ ~LoggerTest() override { nt::DestroyInstance(m_inst); }
+
+ void Generate();
+ void Check(const std::vector<nt::Event>& events, NT_Listener handle,
+ bool infoMsg, bool errMsg);
+
+ protected:
+ NT_Inst m_inst;
+};
+
+void LoggerTest::Generate() {
+ // generate info message
+ nt::StartClient4(m_inst, "");
+
+ // generate error message
+ nt::Publish(nt::Handle(nt::Handle{m_inst}.GetInst(), 5, nt::Handle::kTopic),
+ NT_DOUBLE, "");
+}
+
+void LoggerTest::Check(const std::vector<nt::Event>& events, NT_Listener handle,
+ bool infoMsg, bool errMsg) {
+ size_t count = (infoMsg ? 1u : 0u) + (errMsg ? 1u : 0u);
+ ASSERT_EQ(events.size(), count);
+ for (size_t i = 0; i < count; ++i) {
+ ASSERT_EQ(events[i].listener, handle);
+ ASSERT_EQ(events[i].flags & nt::EventFlags::kLogMessage,
+ nt::EventFlags::kLogMessage);
+ auto log = events[i].GetLogMessage();
+ ASSERT_TRUE(log);
+ if (infoMsg) {
+ ASSERT_EQ(log->message, "starting network client");
+ ASSERT_EQ(log->level, NT_LOG_INFO);
+ infoMsg = false;
+ } else if (errMsg) {
+ ASSERT_EQ(log->message,
+ "trying to publish invalid topic handle (386924549)");
+ ASSERT_EQ(log->level, NT_LOG_ERROR);
+ errMsg = false;
+ }
+ }
+}
+
+TEST_F(LoggerTest, DefaultLogRange) {
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto handle =
+ nt::AddPolledListener(poller, m_inst, nt::EventFlags::kLogMessage);
+
+ Generate();
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+
+ Check(events, handle, true, true);
+}
+
+TEST_F(LoggerTest, InfoOnly) {
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto handle = nt::AddPolledLogger(poller, NT_LOG_INFO, NT_LOG_INFO);
+
+ Generate();
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+
+ Check(events, handle, true, false);
+}
+
+TEST_F(LoggerTest, Error) {
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto handle = nt::AddPolledLogger(poller, NT_LOG_ERROR, 100);
+
+ Generate();
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+
+ Check(events, handle, false, true);
+}
diff --git a/ntcore/src/test/native/cpp/MessageMatcher.cpp b/ntcore/src/test/native/cpp/MessageMatcher.cpp
deleted file mode 100644
index 69c87a0..0000000
--- a/ntcore/src/test/native/cpp/MessageMatcher.cpp
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "MessageMatcher.h"
-
-namespace nt {
-
-bool MessageMatcher::MatchAndExplain(
- std::shared_ptr<Message> msg,
- ::testing::MatchResultListener* listener) const {
- bool match = true;
- if (!msg) {
- return false;
- }
- if (msg->str() != goodmsg->str()) {
- *listener << "str mismatch ";
- match = false;
- }
- if ((!msg->value() && goodmsg->value()) ||
- (msg->value() && !goodmsg->value()) ||
- (msg->value() && goodmsg->value() &&
- *msg->value() != *goodmsg->value())) {
- *listener << "value mismatch ";
- match = false;
- }
- if (msg->id() != goodmsg->id()) {
- *listener << "id mismatch ";
- match = false;
- }
- if (msg->flags() != goodmsg->flags()) {
- *listener << "flags mismatch";
- match = false;
- }
- if (msg->seq_num_uid() != goodmsg->seq_num_uid()) {
- *listener << "seq_num_uid mismatch";
- match = false;
- }
- return match;
-}
-
-void MessageMatcher::DescribeTo(::std::ostream* os) const {
- PrintTo(goodmsg, os);
-}
-
-void MessageMatcher::DescribeNegationTo(::std::ostream* os) const {
- *os << "is not equal to ";
- PrintTo(goodmsg, os);
-}
-
-} // namespace nt
diff --git a/ntcore/src/test/native/cpp/MessageMatcher.h b/ntcore/src/test/native/cpp/MessageMatcher.h
deleted file mode 100644
index 7afeeef..0000000
--- a/ntcore/src/test/native/cpp/MessageMatcher.h
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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.
-
-#ifndef NTCORE_MESSAGEMATCHER_H_
-#define NTCORE_MESSAGEMATCHER_H_
-
-#include <memory>
-#include <ostream>
-#include <utility>
-
-#include "Message.h"
-#include "TestPrinters.h"
-#include "gmock/gmock.h"
-
-namespace nt {
-
-class MessageMatcher
- : public ::testing::MatcherInterface<std::shared_ptr<Message>> {
- public:
- explicit MessageMatcher(std::shared_ptr<Message> goodmsg_)
- : goodmsg(std::move(goodmsg_)) {}
-
- bool MatchAndExplain(std::shared_ptr<Message> msg,
- ::testing::MatchResultListener* listener) const override;
- void DescribeTo(::std::ostream* os) const override;
- void DescribeNegationTo(::std::ostream* os) const override;
-
- private:
- std::shared_ptr<Message> goodmsg;
-};
-
-inline ::testing::Matcher<std::shared_ptr<Message>> MessageEq(
- std::shared_ptr<Message> goodmsg) {
- return ::testing::MakeMatcher(new MessageMatcher(goodmsg));
-}
-
-} // namespace nt
-
-#endif // NTCORE_MESSAGEMATCHER_H_
diff --git a/ntcore/src/test/native/cpp/MockConnectionList.h b/ntcore/src/test/native/cpp/MockConnectionList.h
new file mode 100644
index 0000000..cd93343
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockConnectionList.h
@@ -0,0 +1,24 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <vector>
+
+#include "IConnectionList.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MockConnectionList : public IConnectionList {
+ public:
+ MOCK_METHOD(int, AddConnection, (const ConnectionInfo& info), (override));
+ MOCK_METHOD(void, RemoveConnection, (int handle), (override));
+ MOCK_METHOD(void, ClearConnections, (), (override));
+ MOCK_METHOD(std::vector<ConnectionInfo>, GetConnections, (),
+ (const, override));
+ MOCK_METHOD(bool, IsConnected, (), (const, override));
+};
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/MockConnectionNotifier.h b/ntcore/src/test/native/cpp/MockConnectionNotifier.h
deleted file mode 100644
index d632d5c..0000000
--- a/ntcore/src/test/native/cpp/MockConnectionNotifier.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// 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.
-
-#ifndef NTCORE_MOCKCONNECTIONNOTIFIER_H_
-#define NTCORE_MOCKCONNECTIONNOTIFIER_H_
-
-#include "IConnectionNotifier.h"
-#include "gmock/gmock.h"
-
-namespace nt {
-
-class MockConnectionNotifier : public IConnectionNotifier {
- public:
- MOCK_METHOD1(
- Add,
- unsigned int(
- std::function<void(const ConnectionNotification& event)> callback));
- MOCK_METHOD1(AddPolled, unsigned int(unsigned int poller_uid));
- MOCK_METHOD3(NotifyConnection,
- void(bool connected, const ConnectionInfo& conn_info,
- unsigned int only_listener));
-};
-
-} // namespace nt
-
-#endif // NTCORE_MOCKCONNECTIONNOTIFIER_H_
diff --git a/ntcore/src/test/native/cpp/MockDispatcher.h b/ntcore/src/test/native/cpp/MockDispatcher.h
deleted file mode 100644
index 22b0fba..0000000
--- a/ntcore/src/test/native/cpp/MockDispatcher.h
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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.
-
-#ifndef NTCORE_MOCKDISPATCHER_H_
-#define NTCORE_MOCKDISPATCHER_H_
-
-#include <memory>
-
-#include "IDispatcher.h"
-#include "gmock/gmock.h"
-
-namespace nt {
-
-class MockDispatcher : public IDispatcher {
- public:
- MOCK_METHOD3(QueueOutgoing,
- void(std::shared_ptr<Message> msg, INetworkConnection* only,
- INetworkConnection* except));
-};
-
-} // namespace nt
-
-#endif // NTCORE_MOCKDISPATCHER_H_
diff --git a/ntcore/src/test/native/cpp/MockEntryNotifier.h b/ntcore/src/test/native/cpp/MockEntryNotifier.h
deleted file mode 100644
index 58518c6..0000000
--- a/ntcore/src/test/native/cpp/MockEntryNotifier.h
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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.
-
-#ifndef NTCORE_MOCKENTRYNOTIFIER_H_
-#define NTCORE_MOCKENTRYNOTIFIER_H_
-
-#include <memory>
-
-#include "IEntryNotifier.h"
-#include "gmock/gmock.h"
-
-namespace nt {
-
-class MockEntryNotifier : public IEntryNotifier {
- public:
- MOCK_CONST_METHOD0(local_notifiers, bool());
- MOCK_METHOD3(
- Add,
- unsigned int(std::function<void(const EntryNotification& event)> callback,
- std::string_view prefix, unsigned int flags));
- MOCK_METHOD3(
- Add,
- unsigned int(std::function<void(const EntryNotification& event)> callback,
- unsigned int local_id, unsigned int flags));
- MOCK_METHOD3(AddPolled,
- unsigned int(unsigned int poller_uid, std::string_view prefix,
- unsigned int flags));
- MOCK_METHOD3(AddPolled,
- unsigned int(unsigned int poller_uid, unsigned int local_id,
- unsigned int flags));
- MOCK_METHOD5(NotifyEntry,
- void(unsigned int local_id, std::string_view name,
- std::shared_ptr<Value> value, unsigned int flags,
- unsigned int only_listener));
-};
-
-} // namespace nt
-
-#endif // NTCORE_MOCKENTRYNOTIFIER_H_
diff --git a/ntcore/src/test/native/cpp/MockListenerStorage.h b/ntcore/src/test/native/cpp/MockListenerStorage.h
new file mode 100644
index 0000000..6e1dc4e
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockListenerStorage.h
@@ -0,0 +1,45 @@
+// 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 <memory>
+#include <span>
+#include <string_view>
+
+#include "IListenerStorage.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class MockListenerStorage : public IListenerStorage {
+ public:
+ MOCK_METHOD(void, Activate,
+ (NT_Listener listenerHandle, unsigned int mask,
+ FinishEventFunc finishEvent),
+ (override));
+ MOCK_METHOD(void, Notify,
+ (std::span<const NT_Listener> handles, unsigned int flags,
+ std::span<ConnectionInfo const* const> infos),
+ (override));
+ MOCK_METHOD(void, Notify,
+ (std::span<const NT_Listener> handles, unsigned int flags,
+ std::span<const TopicInfo> infos),
+ (override));
+ MOCK_METHOD(void, Notify,
+ (std::span<const NT_Listener> handles, unsigned int flags,
+ NT_Topic topic, NT_Handle subentry, const Value& value),
+ (override));
+ MOCK_METHOD(void, Notify,
+ (unsigned int flags, unsigned int level,
+ std::string_view filename, unsigned int line,
+ std::string_view message),
+ (override));
+ MOCK_METHOD(void, NotifyTimeSync,
+ (std::span<const NT_Listener> handles, unsigned int flags,
+ int64_t serverTimeOffset, int64_t rtt2, bool valid),
+ (override));
+};
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/MockLogger.h b/ntcore/src/test/native/cpp/MockLogger.h
new file mode 100644
index 0000000..f20a12f
--- /dev/null
+++ b/ntcore/src/test/native/cpp/MockLogger.h
@@ -0,0 +1,24 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <wpi/Logger.h>
+
+#include "gmock/gmock.h"
+
+namespace wpi {
+
+class MockLogger : public Logger,
+ public ::testing::MockFunction<void(
+ unsigned int level, std::string_view file,
+ unsigned int line, std::string_view msg)> {
+ public:
+ MockLogger() {
+ SetLogger([this](unsigned int level, const char* file, unsigned int line,
+ const char* msg) { Call(level, file, line, msg); });
+ }
+};
+
+} // namespace wpi
diff --git a/ntcore/src/test/native/cpp/MockNetworkConnection.h b/ntcore/src/test/native/cpp/MockNetworkConnection.h
deleted file mode 100644
index be9c2c6..0000000
--- a/ntcore/src/test/native/cpp/MockNetworkConnection.h
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.
-
-#ifndef NTCORE_MOCKNETWORKCONNECTION_H_
-#define NTCORE_MOCKNETWORKCONNECTION_H_
-
-#include <memory>
-
-#include "INetworkConnection.h"
-#include "gmock/gmock.h"
-
-namespace nt {
-
-class MockNetworkConnection : public INetworkConnection {
- public:
- MOCK_CONST_METHOD0(info, ConnectionInfo());
-
- MOCK_METHOD1(QueueOutgoing, void(std::shared_ptr<Message> msg));
- MOCK_METHOD1(PostOutgoing, void(bool keep_alive));
-
- MOCK_CONST_METHOD0(proto_rev, unsigned int());
- MOCK_METHOD1(set_proto_rev, void(unsigned int proto_rev));
-
- MOCK_CONST_METHOD0(state, State());
- MOCK_METHOD1(set_state, void(State state));
-};
-
-} // namespace nt
-
-#endif // NTCORE_MOCKNETWORKCONNECTION_H_
diff --git a/ntcore/src/test/native/cpp/MockRpcServer.h b/ntcore/src/test/native/cpp/MockRpcServer.h
deleted file mode 100644
index be9e512..0000000
--- a/ntcore/src/test/native/cpp/MockRpcServer.h
+++ /dev/null
@@ -1,26 +0,0 @@
-// 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.
-
-#ifndef NTCORE_MOCKRPCSERVER_H_
-#define NTCORE_MOCKRPCSERVER_H_
-
-#include "IRpcServer.h"
-#include "gmock/gmock.h"
-
-namespace nt {
-
-class MockRpcServer : public IRpcServer {
- public:
- MOCK_METHOD0(Start, void());
- MOCK_METHOD1(RemoveRpc, void(unsigned int rpc_uid));
- MOCK_METHOD7(ProcessRpc,
- void(unsigned int local_id, unsigned int call_uid,
- std::string_view name, std::string_view params,
- const ConnectionInfo& conn, SendResponseFunc send_response,
- unsigned int rpc_uid));
-};
-
-} // namespace nt
-
-#endif // NTCORE_MOCKRPCSERVER_H_
diff --git a/ntcore/src/test/native/cpp/NetworkTableTest.cpp b/ntcore/src/test/native/cpp/NetworkTableTest.cpp
index 73c4786..aa28afd 100644
--- a/ntcore/src/test/native/cpp/NetworkTableTest.cpp
+++ b/ntcore/src/test/native/cpp/NetworkTableTest.cpp
@@ -89,3 +89,15 @@
ASSERT_TRUE(inst.GetEntry("/testkey").Exists());
nt::NetworkTableInstance::Destroy(inst);
}
+
+TEST_F(NetworkTableTest, ResetInstance) {
+ auto inst = nt::NetworkTableInstance::Create();
+ auto nt = inst.GetTable("containskey");
+ ASSERT_FALSE(nt->ContainsKey("testkey"));
+ nt->PutNumber("testkey", 5);
+ ASSERT_TRUE(nt->ContainsKey("testkey"));
+ ASSERT_TRUE(inst.GetEntry("/containskey/testkey").Exists());
+ nt::ResetInstance(inst.GetHandle());
+ ASSERT_FALSE(nt->ContainsKey("testkey"));
+ nt::NetworkTableInstance::Destroy(inst);
+}
diff --git a/ntcore/src/test/native/cpp/PubSubOptionsMatcher.cpp b/ntcore/src/test/native/cpp/PubSubOptionsMatcher.cpp
new file mode 100644
index 0000000..3437a91
--- /dev/null
+++ b/ntcore/src/test/native/cpp/PubSubOptionsMatcher.cpp
@@ -0,0 +1,43 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "PubSubOptionsMatcher.h"
+
+#include "TestPrinters.h"
+
+namespace nt {
+
+bool PubSubOptionsMatcher::MatchAndExplain(
+ const PubSubOptionsImpl& val,
+ ::testing::MatchResultListener* listener) const {
+ bool match = true;
+ if (val.periodicMs != good.periodicMs) {
+ *listener << "periodic mismatch ";
+ match = false;
+ }
+ if (val.pollStorage != good.pollStorage) {
+ *listener << "pollStorage mismatch ";
+ match = false;
+ }
+ if (val.sendAll != good.sendAll) {
+ *listener << "sendAll mismatch ";
+ match = false;
+ }
+ if (val.keepDuplicates != good.keepDuplicates) {
+ *listener << "keepDuplicates mismatch ";
+ match = false;
+ }
+ return match;
+}
+
+void PubSubOptionsMatcher::DescribeTo(::std::ostream* os) const {
+ PrintTo(good, os);
+}
+
+void PubSubOptionsMatcher::DescribeNegationTo(::std::ostream* os) const {
+ *os << "is not equal to ";
+ PrintTo(good, os);
+}
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/PubSubOptionsMatcher.h b/ntcore/src/test/native/cpp/PubSubOptionsMatcher.h
new file mode 100644
index 0000000..925f17d
--- /dev/null
+++ b/ntcore/src/test/native/cpp/PubSubOptionsMatcher.h
@@ -0,0 +1,35 @@
+// 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 <ostream>
+#include <utility>
+
+#include "PubSubOptions.h"
+#include "gmock/gmock.h"
+
+namespace nt {
+
+class PubSubOptionsMatcher
+ : public ::testing::MatcherInterface<const PubSubOptionsImpl&> {
+ public:
+ explicit PubSubOptionsMatcher(PubSubOptionsImpl good)
+ : good{std::move(good)} {}
+
+ bool MatchAndExplain(const PubSubOptionsImpl& val,
+ ::testing::MatchResultListener* listener) const override;
+ void DescribeTo(::std::ostream* os) const override;
+ void DescribeNegationTo(::std::ostream* os) const override;
+
+ private:
+ PubSubOptionsImpl good;
+};
+
+inline ::testing::Matcher<const PubSubOptionsImpl&> PubSubOptionsEq(
+ PubSubOptionsImpl good) {
+ return ::testing::MakeMatcher(new PubSubOptionsMatcher(std::move(good)));
+}
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/SpanMatcher.h b/ntcore/src/test/native/cpp/SpanMatcher.h
new file mode 100644
index 0000000..9973c03
--- /dev/null
+++ b/ntcore/src/test/native/cpp/SpanMatcher.h
@@ -0,0 +1,72 @@
+// 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 <algorithm>
+#include <initializer_list>
+#include <memory>
+#include <ostream>
+#include <span>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "TestPrinters.h"
+#include "gmock/gmock.h"
+
+namespace wpi {
+
+template <typename T>
+class SpanMatcher : public ::testing::MatcherInterface<std::span<T>> {
+ public:
+ explicit SpanMatcher(std::span<T> good_) : good{good_.begin(), good_.end()} {}
+
+ bool MatchAndExplain(std::span<T> val,
+ ::testing::MatchResultListener* listener) const override;
+ void DescribeTo(::std::ostream* os) const override;
+ void DescribeNegationTo(::std::ostream* os) const override;
+
+ private:
+ std::vector<std::remove_cv_t<T>> good;
+};
+
+template <typename T>
+inline ::testing::Matcher<std::span<const T>> SpanEq(std::span<const T> good) {
+ return ::testing::MakeMatcher(new SpanMatcher(good));
+}
+
+template <typename T>
+inline ::testing::Matcher<std::span<const T>> SpanEq(
+ std::initializer_list<const T> good) {
+ return ::testing::MakeMatcher(
+ new SpanMatcher<const T>({good.begin(), good.end()}));
+}
+
+template <typename T>
+bool SpanMatcher<T>::MatchAndExplain(
+ std::span<T> val, ::testing::MatchResultListener* listener) const {
+ if (val.size() != good.size() ||
+ !std::equal(val.begin(), val.end(), good.begin())) {
+ return false;
+ }
+ return true;
+}
+
+template <typename T>
+void SpanMatcher<T>::DescribeTo(::std::ostream* os) const {
+ PrintTo(std::span<T>{good}, os);
+}
+
+template <typename T>
+void SpanMatcher<T>::DescribeNegationTo(::std::ostream* os) const {
+ *os << "is not equal to ";
+ PrintTo(std::span<T>{good}, os);
+}
+
+} // namespace wpi
+
+inline std::span<const uint8_t> operator"" _us(const char* str, size_t len) {
+ return {reinterpret_cast<const uint8_t*>(str), len};
+}
diff --git a/ntcore/src/test/native/cpp/StorageTest.cpp b/ntcore/src/test/native/cpp/StorageTest.cpp
deleted file mode 100644
index e1ee1c7..0000000
--- a/ntcore/src/test/native/cpp/StorageTest.cpp
+++ /dev/null
@@ -1,1003 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include "StorageTest.h"
-
-#include <wpi/SmallString.h>
-#include <wpi/StringExtras.h>
-#include <wpi/raw_istream.h>
-#include <wpi/raw_ostream.h>
-
-#include "MessageMatcher.h"
-#include "MockNetworkConnection.h"
-#include "Storage.h"
-#include "TestPrinters.h"
-#include "ValueMatcher.h"
-#include "gmock/gmock.h"
-#include "gtest/gtest.h"
-
-using ::testing::_;
-using ::testing::AnyNumber;
-using ::testing::IsNull;
-using ::testing::Return;
-
-namespace nt {
-
-class StorageEmptyTest : public StorageTest,
- public ::testing::TestWithParam<bool> {
- public:
- StorageEmptyTest() {
- HookOutgoing(GetParam());
- EXPECT_CALL(notifier, local_notifiers())
- .Times(AnyNumber())
- .WillRepeatedly(Return(true));
- }
-};
-
-class StoragePopulateOneTest : public StorageEmptyTest {
- public:
- StoragePopulateOneTest() {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, local_notifiers())
- .Times(AnyNumber())
- .WillRepeatedly(Return(false));
- storage.SetEntryTypeValue("foo", Value::MakeBoolean(true));
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- ::testing::Mock::VerifyAndClearExpectations(¬ifier);
- EXPECT_CALL(notifier, local_notifiers())
- .Times(AnyNumber())
- .WillRepeatedly(Return(true));
- }
-};
-
-class StoragePopulatedTest : public StorageEmptyTest {
- public:
- StoragePopulatedTest() {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, local_notifiers())
- .Times(AnyNumber())
- .WillRepeatedly(Return(false));
- storage.SetEntryTypeValue("foo", Value::MakeBoolean(true));
- storage.SetEntryTypeValue("foo2", Value::MakeDouble(0.0));
- storage.SetEntryTypeValue("bar", Value::MakeDouble(1.0));
- storage.SetEntryTypeValue("bar2", Value::MakeBoolean(false));
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- ::testing::Mock::VerifyAndClearExpectations(¬ifier);
- EXPECT_CALL(notifier, local_notifiers())
- .Times(AnyNumber())
- .WillRepeatedly(Return(true));
- }
-};
-
-class StoragePersistentTest : public StorageEmptyTest {
- public:
- StoragePersistentTest() {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, local_notifiers())
- .Times(AnyNumber())
- .WillRepeatedly(Return(false));
- storage.SetEntryTypeValue("boolean/true", Value::MakeBoolean(true));
- storage.SetEntryTypeValue("boolean/false", Value::MakeBoolean(false));
- storage.SetEntryTypeValue("double/neg", Value::MakeDouble(-1.5));
- storage.SetEntryTypeValue("double/zero", Value::MakeDouble(0.0));
- storage.SetEntryTypeValue("double/big", Value::MakeDouble(1.3e8));
- storage.SetEntryTypeValue("string/empty", Value::MakeString(""));
- storage.SetEntryTypeValue("string/normal", Value::MakeString("hello"));
- storage.SetEntryTypeValue(
- "string/special", Value::MakeString(std::string_view("\0\3\5\n", 4)));
- storage.SetEntryTypeValue("string/quoted", Value::MakeString("\"a\""));
- storage.SetEntryTypeValue("raw/empty", Value::MakeRaw(""));
- storage.SetEntryTypeValue("raw/normal", Value::MakeRaw("hello"));
- storage.SetEntryTypeValue("raw/special",
- Value::MakeRaw(std::string_view("\0\3\5\n", 4)));
- storage.SetEntryTypeValue("booleanarr/empty",
- Value::MakeBooleanArray(std::vector<int>{}));
- storage.SetEntryTypeValue("booleanarr/one",
- Value::MakeBooleanArray(std::vector<int>{1}));
- storage.SetEntryTypeValue("booleanarr/two",
- Value::MakeBooleanArray(std::vector<int>{1, 0}));
- storage.SetEntryTypeValue("doublearr/empty",
- Value::MakeDoubleArray(std::vector<double>{}));
- storage.SetEntryTypeValue("doublearr/one",
- Value::MakeDoubleArray(std::vector<double>{0.5}));
- storage.SetEntryTypeValue(
- "doublearr/two",
- Value::MakeDoubleArray(std::vector<double>{0.5, -0.25}));
- storage.SetEntryTypeValue(
- "stringarr/empty", Value::MakeStringArray(std::vector<std::string>{}));
- storage.SetEntryTypeValue(
- "stringarr/one",
- Value::MakeStringArray(std::vector<std::string>{"hello"}));
- storage.SetEntryTypeValue(
- "stringarr/two",
- Value::MakeStringArray(std::vector<std::string>{"hello", "world\n"}));
- storage.SetEntryTypeValue(std::string_view("\0\3\5\n", 4),
- Value::MakeBoolean(true));
- storage.SetEntryTypeValue("=", Value::MakeBoolean(true));
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- ::testing::Mock::VerifyAndClearExpectations(¬ifier);
- EXPECT_CALL(notifier, local_notifiers())
- .Times(AnyNumber())
- .WillRepeatedly(Return(true));
- }
-};
-
-class MockLoadWarn {
- public:
- MOCK_METHOD2(Warn, void(size_t line, std::string_view msg));
-};
-
-TEST_P(StorageEmptyTest, Construct) {
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, StorageEntryInit) {
- auto entry = GetEntry("foo");
- EXPECT_FALSE(entry->value);
- EXPECT_EQ(0u, entry->flags);
- EXPECT_EQ("foobar", entry->name); // since GetEntry uses the tmp_entry.
- EXPECT_EQ(0xffffu, entry->id);
- EXPECT_EQ(SequenceNumber(), entry->seq_num);
-}
-
-TEST_P(StorageEmptyTest, GetEntryValueNotExist) {
- EXPECT_FALSE(storage.GetEntryValue("foo"));
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, GetEntryValueExist) {
- auto value = Value::MakeBoolean(true);
- EXPECT_CALL(dispatcher, QueueOutgoing(_, IsNull(), IsNull()));
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _));
- storage.SetEntryTypeValue("foo", value);
- EXPECT_EQ(value, storage.GetEntryValue("foo"));
-}
-
-TEST_P(StorageEmptyTest, SetEntryTypeValueAssignNew) {
- // brand new entry
- auto value = Value::MakeBoolean(true);
- // id assigned if server
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryAssign(
- "foo", GetParam() ? 0 : 0xffff, 1, value, 0)),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier, NotifyEntry(0, std::string_view("foo"), value,
- NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
-
- storage.SetEntryTypeValue("foo", value);
- EXPECT_EQ(value, GetEntry("foo")->value);
- if (GetParam()) {
- ASSERT_EQ(1u, idmap().size());
- EXPECT_EQ(value, idmap()[0]->value);
- } else {
- EXPECT_TRUE(idmap().empty());
- }
-}
-
-TEST_P(StoragePopulateOneTest, SetEntryTypeValueAssignTypeChange) {
- // update with different type results in assignment message
- auto value = Value::MakeDouble(0.0);
-
- // id assigned if server; seq_num incremented
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryAssign(
- "foo", GetParam() ? 0 : 0xffff, 2, value, 0)),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier,
- NotifyEntry(0, std::string_view("foo"), value,
- NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
-
- storage.SetEntryTypeValue("foo", value);
- EXPECT_EQ(value, GetEntry("foo")->value);
-}
-
-TEST_P(StoragePopulateOneTest, SetEntryTypeValueEqualValue) {
- // update with same type and same value: change value contents but no update
- // message is issued (minimizing bandwidth usage)
- auto value = Value::MakeBoolean(true);
- storage.SetEntryTypeValue("foo", value);
- EXPECT_EQ(value, GetEntry("foo")->value);
-}
-
-TEST_P(StoragePopulatedTest, SetEntryTypeValueDifferentValue) {
- // update with same type and different value results in value update message
- auto value = Value::MakeDouble(1.0);
-
- // client shouldn't send an update as id not assigned yet
- if (GetParam()) {
- // id assigned if server; seq_num incremented
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(notifier,
- NotifyEntry(1, std::string_view("foo2"), value,
- NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
- storage.SetEntryTypeValue("foo2", value);
- EXPECT_EQ(value, GetEntry("foo2")->value);
-
- if (!GetParam()) {
- // seq_num should still be incremented
- EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
- }
-}
-
-TEST_P(StorageEmptyTest, SetEntryTypeValueEmptyName) {
- auto value = Value::MakeBoolean(true);
- storage.SetEntryTypeValue("", value);
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, SetEntryTypeValueEmptyValue) {
- storage.SetEntryTypeValue("foo", nullptr);
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, SetEntryValueAssignNew) {
- // brand new entry
- auto value = Value::MakeBoolean(true);
-
- // id assigned if server
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryAssign(
- "foo", GetParam() ? 0 : 0xffff, 1, value, 0)),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier, NotifyEntry(0, std::string_view("foo"), value,
- NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
-
- EXPECT_TRUE(storage.SetEntryValue("foo", value));
- EXPECT_EQ(value, GetEntry("foo")->value);
-}
-
-TEST_P(StoragePopulateOneTest, SetEntryValueAssignTypeChange) {
- // update with different type results in error and no message or notification
- auto value = Value::MakeDouble(0.0);
- EXPECT_FALSE(storage.SetEntryValue("foo", value));
- auto entry = GetEntry("foo");
- EXPECT_NE(value, entry->value);
-}
-
-TEST_P(StoragePopulateOneTest, SetEntryValueEqualValue) {
- // update with same type and same value: change value contents but no update
- // message is issued (minimizing bandwidth usage)
- auto value = Value::MakeBoolean(true);
- EXPECT_TRUE(storage.SetEntryValue("foo", value));
- auto entry = GetEntry("foo");
- EXPECT_EQ(value, entry->value);
-}
-
-TEST_P(StoragePopulatedTest, SetEntryValueDifferentValue) {
- // update with same type and different value results in value update message
- auto value = Value::MakeDouble(1.0);
-
- // client shouldn't send an update as id not assigned yet
- if (GetParam()) {
- // id assigned if server; seq_num incremented
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(notifier,
- NotifyEntry(1, std::string_view("foo2"), value,
- NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
-
- EXPECT_TRUE(storage.SetEntryValue("foo2", value));
- auto entry = GetEntry("foo2");
- EXPECT_EQ(value, entry->value);
-
- if (!GetParam()) {
- // seq_num should still be incremented
- EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
- }
-}
-
-TEST_P(StorageEmptyTest, SetEntryValueEmptyName) {
- auto value = Value::MakeBoolean(true);
- EXPECT_TRUE(storage.SetEntryValue("", value));
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, SetEntryValueEmptyValue) {
- EXPECT_TRUE(storage.SetEntryValue("foo", nullptr));
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, SetDefaultEntryAssignNew) {
- // brand new entry
- auto value = Value::MakeBoolean(true);
-
- // id assigned if server
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryAssign(
- "foo", GetParam() ? 0 : 0xffff, 1, value, 0)),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier, NotifyEntry(0, std::string_view("foo"), value,
- NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
-
- auto ret_val = storage.SetDefaultEntryValue("foo", value);
- EXPECT_TRUE(ret_val);
- EXPECT_EQ(value, GetEntry("foo")->value);
-}
-
-TEST_P(StoragePopulateOneTest, SetDefaultEntryExistsSameType) {
- // existing entry
- auto value = Value::MakeBoolean(true);
- auto ret_val = storage.SetDefaultEntryValue("foo", value);
- EXPECT_TRUE(ret_val);
- EXPECT_NE(value, GetEntry("foo")->value);
-}
-
-TEST_P(StoragePopulateOneTest, SetDefaultEntryExistsDifferentType) {
- // existing entry is boolean
- auto value = Value::MakeDouble(2.0);
- auto ret_val = storage.SetDefaultEntryValue("foo", value);
- EXPECT_FALSE(ret_val);
- // should not have updated value in table if it already existed.
- EXPECT_NE(value, GetEntry("foo")->value);
-}
-
-TEST_P(StorageEmptyTest, SetDefaultEntryEmptyName) {
- auto value = Value::MakeBoolean(true);
- auto ret_val = storage.SetDefaultEntryValue("", value);
- EXPECT_FALSE(ret_val);
- auto entry = GetEntry("foo");
- EXPECT_FALSE(entry->value);
- EXPECT_EQ(0u, entry->flags);
- EXPECT_EQ("foobar", entry->name); // since GetEntry uses the tmp_entry.
- EXPECT_EQ(0xffffu, entry->id);
- EXPECT_EQ(SequenceNumber(), entry->seq_num);
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, SetDefaultEntryEmptyValue) {
- auto value = Value::MakeBoolean(true);
- auto ret_val = storage.SetDefaultEntryValue("", nullptr);
- EXPECT_FALSE(ret_val);
- auto entry = GetEntry("foo");
- EXPECT_FALSE(entry->value);
- EXPECT_EQ(0u, entry->flags);
- EXPECT_EQ("foobar", entry->name); // since GetEntry uses the tmp_entry.
- EXPECT_EQ(0xffffu, entry->id);
- EXPECT_EQ(SequenceNumber(), entry->seq_num);
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StoragePopulatedTest, SetDefaultEntryEmptyName) {
- auto value = Value::MakeBoolean(true);
- auto ret_val = storage.SetDefaultEntryValue("", value);
- EXPECT_FALSE(ret_val);
- // assert that no entries get added
- EXPECT_EQ(4u, entries().size());
- if (GetParam()) {
- EXPECT_EQ(4u, idmap().size());
- } else {
- EXPECT_EQ(0u, idmap().size());
- }
-}
-
-TEST_P(StoragePopulatedTest, SetDefaultEntryEmptyValue) {
- auto value = Value::MakeBoolean(true);
- auto ret_val = storage.SetDefaultEntryValue("", nullptr);
- EXPECT_FALSE(ret_val);
- // assert that no entries get added
- EXPECT_EQ(4u, entries().size());
- if (GetParam()) {
- EXPECT_EQ(4u, idmap().size());
- } else {
- EXPECT_EQ(0u, idmap().size());
- }
-}
-
-TEST_P(StorageEmptyTest, SetEntryFlagsNew) {
- // flags setting doesn't create an entry
- storage.SetEntryFlags("foo", 0u);
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StoragePopulateOneTest, SetEntryFlagsEqualValue) {
- // update with same value: no update message is issued (minimizing bandwidth
- // usage)
- storage.SetEntryFlags("foo", 0u);
- auto entry = GetEntry("foo");
- EXPECT_EQ(0u, entry->flags);
-}
-
-TEST_P(StoragePopulatedTest, SetEntryFlagsDifferentValue) {
- // update with different value results in flags update message
- // client shouldn't send an update as id not assigned yet
- if (GetParam()) {
- // id assigned as this is the server
- EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::FlagsUpdate(1, 1)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(notifier,
- NotifyEntry(1, std::string_view("foo2"), _,
- NT_NOTIFY_FLAGS | NT_NOTIFY_LOCAL, UINT_MAX));
- storage.SetEntryFlags("foo2", 1u);
- EXPECT_EQ(1u, GetEntry("foo2")->flags);
-}
-
-TEST_P(StorageEmptyTest, SetEntryFlagsEmptyName) {
- storage.SetEntryFlags("", 0u);
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, GetEntryFlagsNotExist) {
- EXPECT_EQ(0u, storage.GetEntryFlags("foo"));
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StoragePopulateOneTest, GetEntryFlagsExist) {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _));
- storage.SetEntryFlags("foo", 1u);
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- EXPECT_EQ(1u, storage.GetEntryFlags("foo"));
-}
-
-TEST_P(StorageEmptyTest, DeleteEntryNotExist) {
- storage.DeleteEntry("foo");
-}
-
-TEST_P(StoragePopulatedTest, DeleteEntryExist) {
- // client shouldn't send an update as id not assigned yet
- if (GetParam()) {
- // id assigned as this is the server
- EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::EntryDelete(1)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(
- notifier,
- NotifyEntry(1, std::string_view("foo2"), ValueEq(Value::MakeDouble(0)),
- NT_NOTIFY_DELETE | NT_NOTIFY_LOCAL, UINT_MAX));
-
- storage.DeleteEntry("foo2");
- ASSERT_EQ(1u, entries().count("foo2"));
- EXPECT_EQ(nullptr, entries()["foo2"]->value);
- EXPECT_EQ(0xffffu, entries()["foo2"]->id);
- EXPECT_FALSE(entries()["foo2"]->local_write);
- if (GetParam()) {
- ASSERT_TRUE(idmap().size() >= 2);
- EXPECT_FALSE(idmap()[1]);
- }
-}
-
-TEST_P(StorageEmptyTest, DeleteAllEntriesEmpty) {
- storage.DeleteAllEntries();
- ASSERT_TRUE(entries().empty());
-}
-
-TEST_P(StoragePopulatedTest, DeleteAllEntries) {
- EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::ClearEntries()),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, NT_NOTIFY_DELETE | NT_NOTIFY_LOCAL,
- UINT_MAX))
- .Times(4);
-
- storage.DeleteAllEntries();
- ASSERT_EQ(1u, entries().count("foo2"));
- EXPECT_EQ(nullptr, entries()["foo2"]->value);
-}
-
-TEST_P(StoragePopulatedTest, DeleteAllEntriesPersistent) {
- GetEntry("foo2")->flags = NT_PERSISTENT;
-
- EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::ClearEntries()),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, NT_NOTIFY_DELETE | NT_NOTIFY_LOCAL,
- UINT_MAX))
- .Times(3);
-
- storage.DeleteAllEntries();
- ASSERT_EQ(1u, entries().count("foo2"));
- EXPECT_NE(nullptr, entries()["foo2"]->value);
-}
-
-TEST_P(StoragePopulatedTest, GetEntryInfoAll) {
- auto info = storage.GetEntryInfo(0, "", 0u);
- ASSERT_EQ(4u, info.size());
-}
-
-TEST_P(StoragePopulatedTest, GetEntryInfoPrefix) {
- auto info = storage.GetEntryInfo(0, "foo", 0u);
- ASSERT_EQ(2u, info.size());
- if (info[0].name == "foo") {
- EXPECT_EQ("foo", info[0].name);
- EXPECT_EQ(NT_BOOLEAN, info[0].type);
- EXPECT_EQ("foo2", info[1].name);
- EXPECT_EQ(NT_DOUBLE, info[1].type);
- } else {
- EXPECT_EQ("foo2", info[0].name);
- EXPECT_EQ(NT_DOUBLE, info[0].type);
- EXPECT_EQ("foo", info[1].name);
- EXPECT_EQ(NT_BOOLEAN, info[1].type);
- }
-}
-
-TEST_P(StoragePopulatedTest, GetEntryInfoTypes) {
- auto info = storage.GetEntryInfo(0, "", NT_DOUBLE);
- ASSERT_EQ(2u, info.size());
- EXPECT_EQ(NT_DOUBLE, info[0].type);
- EXPECT_EQ(NT_DOUBLE, info[1].type);
- if (info[0].name == "foo2") {
- EXPECT_EQ("foo2", info[0].name);
- EXPECT_EQ("bar", info[1].name);
- } else {
- EXPECT_EQ("bar", info[0].name);
- EXPECT_EQ("foo2", info[1].name);
- }
-}
-
-TEST_P(StoragePopulatedTest, GetEntryInfoPrefixTypes) {
- auto info = storage.GetEntryInfo(0, "bar", NT_BOOLEAN);
- ASSERT_EQ(1u, info.size());
- EXPECT_EQ("bar2", info[0].name);
- EXPECT_EQ(NT_BOOLEAN, info[0].type);
-}
-
-TEST_P(StoragePersistentTest, SavePersistentEmpty) {
- wpi::SmallString<256> buf;
- wpi::raw_svector_ostream oss(buf);
- storage.SavePersistent(oss, false);
- ASSERT_EQ("[NetworkTables Storage 3.0]\n", oss.str());
-}
-
-TEST_P(StoragePersistentTest, SavePersistent) {
- for (auto& i : entries()) {
- i.getValue()->flags = NT_PERSISTENT;
- }
- wpi::SmallString<256> buf;
- wpi::raw_svector_ostream oss(buf);
- storage.SavePersistent(oss, false);
- std::string_view out = oss.str();
- // std::fputs(out.c_str(), stderr);
- std::string_view line, rem = out;
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("[NetworkTables Storage 3.0]", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("boolean \"\\x00\\x03\\x05\\n\"=true", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("boolean \"\\x3D\"=true", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("boolean \"boolean/false\"=false", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("boolean \"boolean/true\"=true", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array boolean \"booleanarr/empty\"=", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array boolean \"booleanarr/one\"=true", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array boolean \"booleanarr/two\"=true,false", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("double \"double/big\"=1.3e+08", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("double \"double/neg\"=-1.5", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("double \"double/zero\"=0", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array double \"doublearr/empty\"=", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array double \"doublearr/one\"=0.5", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array double \"doublearr/two\"=0.5,-0.25", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("raw \"raw/empty\"=", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("raw \"raw/normal\"=aGVsbG8=", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("raw \"raw/special\"=AAMFCg==", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("string \"string/empty\"=\"\"", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("string \"string/normal\"=\"hello\"", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("string \"string/quoted\"=\"\\\"a\\\"\"", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("string \"string/special\"=\"\\x00\\x03\\x05\\n\"", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array string \"stringarr/empty\"=", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array string \"stringarr/one\"=\"hello\"", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("array string \"stringarr/two\"=\"hello\",\"world\\n\"", line);
- std::tie(line, rem) = wpi::split(rem, '\n');
- ASSERT_EQ("", line);
-}
-
-TEST_P(StorageEmptyTest, LoadPersistentBadHeader) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- wpi::raw_mem_istream iss("");
- EXPECT_CALL(
- warn,
- Warn(1, std::string_view("header line mismatch, ignoring rest of file")));
- EXPECT_FALSE(storage.LoadEntries(iss, "", true, warn_func));
-
- wpi::raw_mem_istream iss2("[NetworkTables");
- EXPECT_CALL(
- warn,
- Warn(1, std::string_view("header line mismatch, ignoring rest of file")));
-
- EXPECT_FALSE(storage.LoadEntries(iss2, "", true, warn_func));
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, LoadPersistentCommentHeader) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- wpi::raw_mem_istream iss(
- "\n; comment\n# comment\n[NetworkTables Storage 3.0]\n");
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, LoadPersistentEmptyName) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- wpi::raw_mem_istream iss("[NetworkTables Storage 3.0]\nboolean \"\"=true\n");
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, LoadPersistentAssign) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- auto value = Value::MakeBoolean(true);
-
- // id assigned if server
- EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::EntryAssign(
- "foo", GetParam() ? 0 : 0xffff, 1,
- value, NT_PERSISTENT)),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier, NotifyEntry(0, std::string_view("foo"),
- ValueEq(Value::MakeBoolean(true)),
- NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX));
-
- wpi::raw_mem_istream iss(
- "[NetworkTables Storage 3.0]\nboolean \"foo\"=true\n");
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
- auto entry = GetEntry("foo");
- EXPECT_EQ(*value, *entry->value);
- EXPECT_EQ(NT_PERSISTENT, entry->flags);
-}
-
-TEST_P(StoragePopulatedTest, LoadPersistentUpdateFlags) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- // client shouldn't send an update as id not assigned yet
- if (GetParam()) {
- // id assigned as this is server
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::FlagsUpdate(1, NT_PERSISTENT)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(
- notifier,
- NotifyEntry(1, std::string_view("foo2"), ValueEq(Value::MakeDouble(0)),
- NT_NOTIFY_FLAGS | NT_NOTIFY_LOCAL, UINT_MAX));
-
- wpi::raw_mem_istream iss(
- "[NetworkTables Storage 3.0]\ndouble \"foo2\"=0.0\n");
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
- auto entry = GetEntry("foo2");
- EXPECT_EQ(*Value::MakeDouble(0.0), *entry->value);
- EXPECT_EQ(NT_PERSISTENT, entry->flags);
-}
-
-TEST_P(StoragePopulatedTest, LoadPersistentUpdateValue) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- GetEntry("foo2")->flags = NT_PERSISTENT;
-
- auto value = Value::MakeDouble(1.0);
-
- // client shouldn't send an update as id not assigned yet
- if (GetParam()) {
- // id assigned as this is the server; seq_num incremented
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(
- notifier,
- NotifyEntry(1, std::string_view("foo2"), ValueEq(Value::MakeDouble(1)),
- NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL, UINT_MAX));
-
- wpi::raw_mem_istream iss(
- "[NetworkTables Storage 3.0]\ndouble \"foo2\"=1.0\n");
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
- auto entry = GetEntry("foo2");
- EXPECT_EQ(*value, *entry->value);
- EXPECT_EQ(NT_PERSISTENT, entry->flags);
-
- if (!GetParam()) {
- // seq_num should still be incremented
- EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
- }
-}
-
-TEST_P(StoragePopulatedTest, LoadPersistentUpdateValueFlags) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- auto value = Value::MakeDouble(1.0);
-
- // client shouldn't send an update as id not assigned yet
- if (GetParam()) {
- // id assigned as this is the server; seq_num incremented
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::EntryUpdate(1, 2, value)),
- IsNull(), IsNull()));
- EXPECT_CALL(dispatcher,
- QueueOutgoing(MessageEq(Message::FlagsUpdate(1, NT_PERSISTENT)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(
- notifier,
- NotifyEntry(1, std::string_view("foo2"), ValueEq(Value::MakeDouble(1)),
- NT_NOTIFY_FLAGS | NT_NOTIFY_UPDATE | NT_NOTIFY_LOCAL,
- UINT_MAX));
-
- wpi::raw_mem_istream iss(
- "[NetworkTables Storage 3.0]\ndouble \"foo2\"=1.0\n");
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
- auto entry = GetEntry("foo2");
- EXPECT_EQ(*value, *entry->value);
- EXPECT_EQ(NT_PERSISTENT, entry->flags);
-
- if (!GetParam()) {
- // seq_num should still be incremented
- EXPECT_EQ(2u, GetEntry("foo2")->seq_num.value());
- }
-}
-
-TEST_P(StorageEmptyTest, LoadPersistent) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- std::string in = "[NetworkTables Storage 3.0]\n";
- in += "boolean \"\\x00\\x03\\x05\\n\"=true\n";
- in += "boolean \"\\x3D\"=true\n";
- in += "boolean \"boolean/false\"=false\n";
- in += "boolean \"boolean/true\"=true\n";
- in += "array boolean \"booleanarr/empty\"=\n";
- in += "array boolean \"booleanarr/one\"=true\n";
- in += "array boolean \"booleanarr/two\"=true,false\n";
- in += "double \"double/big\"=1.3e+08\n";
- in += "double \"double/neg\"=-1.5\n";
- in += "double \"double/zero\"=0\n";
- in += "array double \"doublearr/empty\"=\n";
- in += "array double \"doublearr/one\"=0.5\n";
- in += "array double \"doublearr/two\"=0.5,-0.25\n";
- in += "raw \"raw/empty\"=\n";
- in += "raw \"raw/normal\"=aGVsbG8=\n";
- in += "raw \"raw/special\"=AAMFCg==\n";
- in += "string \"string/empty\"=\"\"\n";
- in += "string \"string/normal\"=\"hello\"\n";
- in += "string \"string/special\"=\"\\x00\\x03\\x05\\n\"\n";
- in += "string \"string/quoted\"=\"\\\"a\\\"\"\n";
- in += "array string \"stringarr/empty\"=\n";
- in += "array string \"stringarr/one\"=\"hello\"\n";
- in += "array string \"stringarr/two\"=\"hello\",\"world\\n\"\n";
-
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(23);
- EXPECT_CALL(notifier,
- NotifyEntry(_, _, _, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL, UINT_MAX))
- .Times(23);
-
- wpi::raw_mem_istream iss(in);
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
- ASSERT_EQ(23u, entries().size());
-
- EXPECT_EQ(*Value::MakeBoolean(true), *storage.GetEntryValue("boolean/true"));
- EXPECT_EQ(*Value::MakeBoolean(false),
- *storage.GetEntryValue("boolean/false"));
- EXPECT_EQ(*Value::MakeDouble(-1.5), *storage.GetEntryValue("double/neg"));
- EXPECT_EQ(*Value::MakeDouble(0.0), *storage.GetEntryValue("double/zero"));
- EXPECT_EQ(*Value::MakeDouble(1.3e8), *storage.GetEntryValue("double/big"));
- EXPECT_EQ(*Value::MakeString(""), *storage.GetEntryValue("string/empty"));
- EXPECT_EQ(*Value::MakeString("hello"),
- *storage.GetEntryValue("string/normal"));
- EXPECT_EQ(*Value::MakeString(std::string_view("\0\3\5\n", 4)),
- *storage.GetEntryValue("string/special"));
- EXPECT_EQ(*Value::MakeString("\"a\""),
- *storage.GetEntryValue("string/quoted"));
- EXPECT_EQ(*Value::MakeRaw(""), *storage.GetEntryValue("raw/empty"));
- EXPECT_EQ(*Value::MakeRaw("hello"), *storage.GetEntryValue("raw/normal"));
- EXPECT_EQ(*Value::MakeRaw(std::string_view("\0\3\5\n", 4)),
- *storage.GetEntryValue("raw/special"));
- EXPECT_EQ(*Value::MakeBooleanArray(std::vector<int>{}),
- *storage.GetEntryValue("booleanarr/empty"));
- EXPECT_EQ(*Value::MakeBooleanArray(std::vector<int>{1}),
- *storage.GetEntryValue("booleanarr/one"));
- EXPECT_EQ(*Value::MakeBooleanArray(std::vector<int>{1, 0}),
- *storage.GetEntryValue("booleanarr/two"));
- EXPECT_EQ(*Value::MakeDoubleArray(std::vector<double>{}),
- *storage.GetEntryValue("doublearr/empty"));
- EXPECT_EQ(*Value::MakeDoubleArray(std::vector<double>{0.5}),
- *storage.GetEntryValue("doublearr/one"));
- EXPECT_EQ(*Value::MakeDoubleArray(std::vector<double>{0.5, -0.25}),
- *storage.GetEntryValue("doublearr/two"));
- EXPECT_EQ(*Value::MakeStringArray(std::vector<std::string>{}),
- *storage.GetEntryValue("stringarr/empty"));
- EXPECT_EQ(*Value::MakeStringArray(std::vector<std::string>{"hello"}),
- *storage.GetEntryValue("stringarr/one"));
- EXPECT_EQ(
- *Value::MakeStringArray(std::vector<std::string>{"hello", "world\n"}),
- *storage.GetEntryValue("stringarr/two"));
- EXPECT_EQ(*Value::MakeBoolean(true),
- *storage.GetEntryValue(std::string_view("\0\3\5\n", 4)));
- EXPECT_EQ(*Value::MakeBoolean(true), *storage.GetEntryValue("="));
-}
-
-TEST_P(StorageEmptyTest, LoadPersistentWarn) {
- MockLoadWarn warn;
- auto warn_func = [&](size_t line, const char* msg) { warn.Warn(line, msg); };
-
- wpi::raw_mem_istream iss(
- "[NetworkTables Storage 3.0]\nboolean \"foo\"=foo\n");
- EXPECT_CALL(
- warn, Warn(2, std::string_view(
- "unrecognized boolean value, not 'true' or 'false'")));
- EXPECT_TRUE(storage.LoadEntries(iss, "", true, warn_func));
-
- EXPECT_TRUE(entries().empty());
- EXPECT_TRUE(idmap().empty());
-}
-
-TEST_P(StorageEmptyTest, ProcessIncomingEntryAssign) {
- auto conn = std::make_shared<MockNetworkConnection>();
- auto value = Value::MakeDouble(1.0);
- if (GetParam()) {
- // id assign message reply generated on the server; sent to everyone
- EXPECT_CALL(
- dispatcher,
- QueueOutgoing(MessageEq(Message::EntryAssign("foo", 0, 0, value, 0)),
- IsNull(), IsNull()));
- }
- EXPECT_CALL(notifier, NotifyEntry(0, std::string_view("foo"), ValueEq(value),
- NT_NOTIFY_NEW, UINT_MAX));
-
- storage.ProcessIncoming(
- Message::EntryAssign("foo", GetParam() ? 0xffff : 0, 0, value, 0),
- conn.get(), conn);
-}
-
-TEST_P(StoragePopulateOneTest, ProcessIncomingEntryAssign) {
- auto conn = std::make_shared<MockNetworkConnection>();
- auto value = Value::MakeDouble(1.0);
- EXPECT_CALL(*conn, proto_rev()).WillRepeatedly(Return(0x0300u));
- if (GetParam()) {
- // server broadcasts new value to all *other* connections
- EXPECT_CALL(
- dispatcher,
- QueueOutgoing(MessageEq(Message::EntryAssign("foo", 0, 1, value, 0)),
- IsNull(), conn.get()));
- }
- EXPECT_CALL(notifier, NotifyEntry(0, std::string_view("foo"), ValueEq(value),
- NT_NOTIFY_UPDATE, UINT_MAX));
-
- storage.ProcessIncoming(Message::EntryAssign("foo", 0, 1, value, 0),
- conn.get(), conn);
-}
-
-TEST_P(StoragePopulateOneTest, ProcessIncomingEntryAssignIgnore) {
- auto conn = std::make_shared<MockNetworkConnection>();
- auto value = Value::MakeDouble(1.0);
- storage.ProcessIncoming(Message::EntryAssign("foo", 0xffff, 1, value, 0),
- conn.get(), conn);
-}
-
-TEST_P(StoragePopulateOneTest, ProcessIncomingEntryAssignWithFlags) {
- auto conn = std::make_shared<MockNetworkConnection>();
- auto value = Value::MakeDouble(1.0);
- EXPECT_CALL(*conn, proto_rev()).WillRepeatedly(Return(0x0300u));
- if (GetParam()) {
- // server broadcasts new value/flags to all *other* connections
- EXPECT_CALL(
- dispatcher,
- QueueOutgoing(MessageEq(Message::EntryAssign("foo", 0, 1, value, 0x2)),
- IsNull(), conn.get()));
- EXPECT_CALL(notifier,
- NotifyEntry(0, std::string_view("foo"), ValueEq(value),
- NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS, UINT_MAX));
- } else {
- // client forces flags back when an assign message is received for an
- // existing entry with different flags
- EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::FlagsUpdate(0, 0)),
- IsNull(), IsNull()));
- EXPECT_CALL(notifier,
- NotifyEntry(0, std::string_view("foo"), ValueEq(value),
- NT_NOTIFY_UPDATE, UINT_MAX));
- }
-
- storage.ProcessIncoming(Message::EntryAssign("foo", 0, 1, value, 0x2),
- conn.get(), conn);
-}
-
-TEST_P(StoragePopulateOneTest, DeleteCheckHandle) {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
- auto handle = storage.GetEntry("foo");
- storage.DeleteEntry("foo");
- storage.SetEntryTypeValue("foo", Value::MakeBoolean(true));
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- ::testing::Mock::VerifyAndClearExpectations(¬ifier);
-
- auto handle2 = storage.GetEntry("foo");
- ASSERT_EQ(handle, handle2);
-}
-
-TEST_P(StoragePopulateOneTest, DeletedEntryFlags) {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
- auto handle = storage.GetEntry("foo");
- storage.SetEntryFlags("foo", 2);
- storage.DeleteEntry("foo");
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- ::testing::Mock::VerifyAndClearExpectations(¬ifier);
-
- EXPECT_EQ(storage.GetEntryFlags("foo"), 0u);
- EXPECT_EQ(storage.GetEntryFlags(handle), 0u);
- storage.SetEntryFlags("foo", 4);
- storage.SetEntryFlags(handle, 4);
- EXPECT_EQ(storage.GetEntryFlags("foo"), 0u);
- EXPECT_EQ(storage.GetEntryFlags(handle), 0u);
-}
-
-TEST_P(StoragePopulateOneTest, DeletedDeleteAllEntries) {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
- storage.DeleteEntry("foo");
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- ::testing::Mock::VerifyAndClearExpectations(¬ifier);
-
- EXPECT_CALL(dispatcher, QueueOutgoing(MessageEq(Message::ClearEntries()),
- IsNull(), IsNull()));
- storage.DeleteAllEntries();
-}
-
-TEST_P(StoragePopulateOneTest, DeletedGetEntries) {
- EXPECT_CALL(dispatcher, QueueOutgoing(_, _, _)).Times(AnyNumber());
- EXPECT_CALL(notifier, NotifyEntry(_, _, _, _, _)).Times(AnyNumber());
- storage.DeleteEntry("foo");
- ::testing::Mock::VerifyAndClearExpectations(&dispatcher);
- ::testing::Mock::VerifyAndClearExpectations(¬ifier);
-
- EXPECT_TRUE(storage.GetEntries("", 0).empty());
-}
-
-INSTANTIATE_TEST_SUITE_P(StorageEmptyTests, StorageEmptyTest,
- ::testing::Bool());
-INSTANTIATE_TEST_SUITE_P(StoragePopulateOneTests, StoragePopulateOneTest,
- ::testing::Bool());
-INSTANTIATE_TEST_SUITE_P(StoragePopulatedTests, StoragePopulatedTest,
- ::testing::Bool());
-INSTANTIATE_TEST_SUITE_P(StoragePersistentTests, StoragePersistentTest,
- ::testing::Bool());
-
-} // namespace nt
diff --git a/ntcore/src/test/native/cpp/StorageTest.h b/ntcore/src/test/native/cpp/StorageTest.h
index 3a1755f..a0cc737 100644
--- a/ntcore/src/test/native/cpp/StorageTest.h
+++ b/ntcore/src/test/native/cpp/StorageTest.h
@@ -2,8 +2,7 @@
// 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.
-#ifndef NTCORE_STORAGETEST_H_
-#define NTCORE_STORAGETEST_H_
+#pragma once
#include <functional>
#include <memory>
@@ -11,15 +10,13 @@
#include "Log.h"
#include "MockDispatcher.h"
-#include "MockEntryNotifier.h"
-#include "MockRpcServer.h"
#include "Storage.h"
namespace nt {
class StorageTest {
public:
- StorageTest() : storage(notifier, rpc_server, logger), tmp_entry("foobar") {}
+ StorageTest() : storage(logger), tmp_entry("foobar") {}
Storage::EntriesMap& entries() { return storage.m_entries; }
Storage::IdMap& idmap() { return storage.m_idmap; }
@@ -32,13 +29,9 @@
void HookOutgoing(bool server) { storage.SetDispatcher(&dispatcher, server); }
wpi::Logger logger;
- ::testing::StrictMock<MockEntryNotifier> notifier;
- ::testing::StrictMock<MockRpcServer> rpc_server;
::testing::StrictMock<MockDispatcher> dispatcher;
Storage storage;
Storage::Entry tmp_entry;
};
} // namespace nt
-
-#endif // NTCORE_STORAGETEST_H_
diff --git a/ntcore/src/test/native/cpp/TableListenerTest.cpp b/ntcore/src/test/native/cpp/TableListenerTest.cpp
new file mode 100644
index 0000000..7fb1947
--- /dev/null
+++ b/ntcore/src/test/native/cpp/TableListenerTest.cpp
@@ -0,0 +1,60 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <memory>
+
+#include "TestPrinters.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "networktables/DoubleTopic.h"
+#include "networktables/NetworkTableInstance.h"
+#include "ntcore_cpp.h"
+
+using ::testing::_;
+
+using MockTableEventListener = testing::MockFunction<void(
+ nt::NetworkTable* table, std::string_view key, const nt::Event& event)>;
+using MockSubTableListener =
+ testing::MockFunction<void(nt::NetworkTable* parent, std::string_view name,
+ std::shared_ptr<nt::NetworkTable> table)>;
+
+class TableListenerTest : public ::testing::Test {
+ public:
+ TableListenerTest() : m_inst(nt::NetworkTableInstance::Create()) {}
+
+ ~TableListenerTest() override { nt::NetworkTableInstance::Destroy(m_inst); }
+
+ void PublishTopics();
+
+ protected:
+ nt::NetworkTableInstance m_inst;
+ nt::DoublePublisher m_foovalue;
+ nt::DoublePublisher m_barvalue;
+ nt::DoublePublisher m_bazvalue;
+};
+
+void TableListenerTest::PublishTopics() {
+ m_foovalue = m_inst.GetDoubleTopic("/foo/foovalue").Publish();
+ m_barvalue = m_inst.GetDoubleTopic("/foo/bar/barvalue").Publish();
+ m_bazvalue = m_inst.GetDoubleTopic("/baz/bazvalue").Publish();
+}
+
+TEST_F(TableListenerTest, AddListener) {
+ auto table = m_inst.GetTable("/foo");
+ MockTableEventListener listener;
+ table->AddListener(NT_EVENT_TOPIC | NT_EVENT_IMMEDIATE,
+ listener.AsStdFunction());
+ EXPECT_CALL(listener, Call(table.get(), "foovalue", _));
+ PublishTopics();
+ EXPECT_TRUE(m_inst.WaitForListenerQueue(1.0));
+}
+
+TEST_F(TableListenerTest, AddSubTableListener) {
+ auto table = m_inst.GetTable("/foo");
+ MockSubTableListener listener;
+ table->AddSubTableListener(listener.AsStdFunction());
+ EXPECT_CALL(listener, Call(table.get(), "bar", _));
+ PublishTopics();
+ EXPECT_TRUE(m_inst.WaitForListenerQueue(1.0));
+}
diff --git a/ntcore/src/test/native/cpp/TestPrinters.cpp b/ntcore/src/test/native/cpp/TestPrinters.cpp
index 49b407b..66afe4e 100644
--- a/ntcore/src/test/native/cpp/TestPrinters.cpp
+++ b/ntcore/src/test/native/cpp/TestPrinters.cpp
@@ -4,55 +4,56 @@
#include "TestPrinters.h"
+#include <wpi/json.h>
+
#include "Handle.h"
-#include "Message.h"
+#include "PubSubOptions.h"
+#include "net/Message.h"
+#include "net3/Message3.h"
#include "networktables/NetworkTableValue.h"
#include "ntcore_cpp.h"
+namespace wpi {
+void PrintTo(const json& val, ::std::ostream* os) {
+ *os << val.dump();
+}
+} // namespace wpi
+
namespace nt {
-void PrintTo(const EntryNotification& event, std::ostream* os) {
- *os << "EntryNotification{listener=";
+void PrintTo(const Event& event, std::ostream* os) {
+ *os << "Event{listener=";
PrintTo(Handle{event.listener}, os);
- *os << ", entry=";
- PrintTo(Handle{event.entry}, os);
- *os << ", name=\"" << event.name << "\", flags=" << event.flags << ", value=";
- PrintTo(event.value, os);
+ *os << ", flags=" << event.flags;
+ // *os << ", name=\"" << event.name << "\", flags=" << event.flags
+ // << "value=";
+ // PrintTo(event.value, os);
*os << '}';
}
void PrintTo(const Handle& handle, std::ostream* os) {
*os << "Handle{";
switch (handle.GetType()) {
- case Handle::kConnectionListener:
- *os << "kConnectionListener";
+ case Handle::kListener:
+ *os << "kListener";
break;
- case Handle::kConnectionListenerPoller:
- *os << "kConnectionListenerPoller";
+ case Handle::kListenerPoller:
+ *os << "kListenerPoller";
break;
case Handle::kEntry:
*os << "kEntry";
break;
- case Handle::kEntryListener:
- *os << "kEntryListener";
- break;
- case Handle::kEntryListenerPoller:
- *os << "kEntryListenerPoller";
- break;
case Handle::kInstance:
*os << "kInstance";
break;
- case Handle::kLogger:
- *os << "kLogger";
+ case Handle::kTopic:
+ *os << "kTopic";
break;
- case Handle::kLoggerPoller:
- *os << "kLoggerPoller";
+ case Handle::kSubscriber:
+ *os << "kSubscriber";
break;
- case Handle::kRpcCall:
- *os << "kRpcCall";
- break;
- case Handle::kRpcCallPoller:
- *os << "kRpcCallPoller";
+ case Handle::kPublisher:
+ *os << "kPublisher";
break;
default:
*os << "UNKNOWN";
@@ -61,46 +62,46 @@
*os << ", " << handle.GetInst() << ", " << handle.GetIndex() << '}';
}
-void PrintTo(const Message& msg, std::ostream* os) {
+void PrintTo(const net3::Message3& msg, std::ostream* os) {
*os << "Message{";
switch (msg.type()) {
- case Message::kKeepAlive:
+ case net3::Message3::kKeepAlive:
*os << "kKeepAlive";
break;
- case Message::kClientHello:
+ case net3::Message3::kClientHello:
*os << "kClientHello";
break;
- case Message::kProtoUnsup:
+ case net3::Message3::kProtoUnsup:
*os << "kProtoUnsup";
break;
- case Message::kServerHelloDone:
+ case net3::Message3::kServerHelloDone:
*os << "kServerHelloDone";
break;
- case Message::kServerHello:
+ case net3::Message3::kServerHello:
*os << "kServerHello";
break;
- case Message::kClientHelloDone:
+ case net3::Message3::kClientHelloDone:
*os << "kClientHelloDone";
break;
- case Message::kEntryAssign:
+ case net3::Message3::kEntryAssign:
*os << "kEntryAssign";
break;
- case Message::kEntryUpdate:
+ case net3::Message3::kEntryUpdate:
*os << "kEntryUpdate";
break;
- case Message::kFlagsUpdate:
+ case net3::Message3::kFlagsUpdate:
*os << "kFlagsUpdate";
break;
- case Message::kEntryDelete:
+ case net3::Message3::kEntryDelete:
*os << "kEntryDelete";
break;
- case Message::kClearEntries:
+ case net3::Message3::kClearEntries:
*os << "kClearEntries";
break;
- case Message::kExecuteRpc:
+ case net3::Message3::kExecuteRpc:
*os << "kExecuteRpc";
break;
- case Message::kRpcResponse:
+ case net3::Message3::kRpcResponse:
*os << "kRpcResponse";
break;
default:
@@ -120,28 +121,37 @@
case NT_UNASSIGNED:
break;
case NT_BOOLEAN:
- *os << (value.GetBoolean() ? "true" : "false");
+ *os << "boolean, " << (value.GetBoolean() ? "true" : "false");
break;
case NT_DOUBLE:
- *os << value.GetDouble();
+ *os << "double, " << value.GetDouble();
+ break;
+ case NT_FLOAT:
+ *os << "float, " << value.GetFloat();
+ break;
+ case NT_INTEGER:
+ *os << "int, " << value.GetInteger();
break;
case NT_STRING:
- *os << '"' << value.GetString() << '"';
+ *os << "string, \"" << value.GetString() << '"';
break;
case NT_RAW:
- *os << ::testing::PrintToString(value.GetRaw());
+ *os << "raw, " << ::testing::PrintToString(value.GetRaw());
break;
case NT_BOOLEAN_ARRAY:
- *os << ::testing::PrintToString(value.GetBooleanArray());
+ *os << "boolean[], " << ::testing::PrintToString(value.GetBooleanArray());
break;
case NT_DOUBLE_ARRAY:
- *os << ::testing::PrintToString(value.GetDoubleArray());
+ *os << "double[], " << ::testing::PrintToString(value.GetDoubleArray());
+ break;
+ case NT_FLOAT_ARRAY:
+ *os << "float[], " << ::testing::PrintToString(value.GetFloatArray());
+ break;
+ case NT_INTEGER_ARRAY:
+ *os << "int[], " << ::testing::PrintToString(value.GetIntegerArray());
break;
case NT_STRING_ARRAY:
- *os << ::testing::PrintToString(value.GetStringArray());
- break;
- case NT_RPC:
- *os << ::testing::PrintToString(value.GetRpc());
+ *os << "string[], " << ::testing::PrintToString(value.GetStringArray());
break;
default:
*os << "UNKNOWN TYPE " << value.type();
@@ -150,4 +160,11 @@
*os << '}';
}
+void PrintTo(const PubSubOptionsImpl& options, std::ostream* os) {
+ *os << "PubSubOptions{periodicMs=" << options.periodicMs
+ << ", pollStorage=" << options.pollStorage
+ << ", sendAll=" << options.sendAll
+ << ", keepDuplicates=" << options.keepDuplicates << '}';
+}
+
} // namespace nt
diff --git a/ntcore/src/test/native/cpp/TestPrinters.h b/ntcore/src/test/native/cpp/TestPrinters.h
index 47c9eb4..2b8f1da 100644
--- a/ntcore/src/test/native/cpp/TestPrinters.h
+++ b/ntcore/src/test/native/cpp/TestPrinters.h
@@ -2,11 +2,10 @@
// 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.
-#ifndef NTCORE_TESTPRINTERS_H_
-#define NTCORE_TESTPRINTERS_H_
+#pragma once
-#include <memory>
#include <ostream>
+#include <span>
#include <string>
#include <string_view>
@@ -14,42 +13,53 @@
namespace wpi {
+class json;
+
inline void PrintTo(std::string_view str, ::std::ostream* os) {
::testing::internal::PrintStringTo(std::string{str}, os);
}
+template <typename T>
+void PrintTo(std::span<T> val, ::std::ostream* os) {
+ *os << '{';
+ bool first = true;
+ for (auto v : val) {
+ if (first) {
+ first = false;
+ } else {
+ *os << ", ";
+ }
+ *os << ::testing::PrintToString(v);
+ }
+ *os << '}';
+}
+
+void PrintTo(const json& val, ::std::ostream* os);
+
} // namespace wpi
namespace nt {
-class EntryNotification;
+namespace net3 {
+class Message3;
+} // namespace net3
+
+namespace net {
+struct ClientMessage;
+struct ServerMessage;
+} // namespace net
+
+class Event;
class Handle;
-class Message;
+class PubSubOptionsImpl;
class Value;
-void PrintTo(const EntryNotification& event, std::ostream* os);
+void PrintTo(const Event& event, std::ostream* os);
void PrintTo(const Handle& handle, std::ostream* os);
-
-void PrintTo(const Message& msg, std::ostream* os);
-
-inline void PrintTo(std::shared_ptr<Message> msg, std::ostream* os) {
- *os << "shared_ptr{";
- if (msg) {
- PrintTo(*msg, os);
- }
- *os << '}';
-}
-
+void PrintTo(const net3::Message3& msg, std::ostream* os);
+void PrintTo(const net::ClientMessage& msg, std::ostream* os);
+void PrintTo(const net::ServerMessage& msg, std::ostream* os);
void PrintTo(const Value& value, std::ostream* os);
-
-inline void PrintTo(std::shared_ptr<Value> value, std::ostream* os) {
- *os << "shared_ptr{";
- if (value) {
- PrintTo(*value, os);
- }
- *os << '}';
-}
+void PrintTo(const PubSubOptionsImpl& options, std::ostream* os);
} // namespace nt
-
-#endif // NTCORE_TESTPRINTERS_H_
diff --git a/ntcore/src/test/native/cpp/TimeSyncTest.cpp b/ntcore/src/test/native/cpp/TimeSyncTest.cpp
new file mode 100644
index 0000000..54e1f7c
--- /dev/null
+++ b/ntcore/src/test/native/cpp/TimeSyncTest.cpp
@@ -0,0 +1,70 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "gtest/gtest.h"
+#include "networktables/NetworkTableInstance.h"
+#include "networktables/NetworkTableListener.h"
+
+class TimeSyncTest : public ::testing::Test {
+ public:
+ TimeSyncTest() : m_inst(nt::NetworkTableInstance::Create()) {}
+
+ ~TimeSyncTest() override { nt::NetworkTableInstance::Destroy(m_inst); }
+
+ protected:
+ nt::NetworkTableInstance m_inst;
+};
+
+TEST_F(TimeSyncTest, TestLocal) {
+ auto offset = m_inst.GetServerTimeOffset();
+ ASSERT_FALSE(offset);
+}
+
+TEST_F(TimeSyncTest, TestServer) {
+ nt::NetworkTableListenerPoller poller{m_inst};
+ poller.AddTimeSyncListener(false);
+
+ m_inst.StartServer("timesynctest.json", "127.0.0.1", 0, 10030);
+ auto offset = m_inst.GetServerTimeOffset();
+ ASSERT_TRUE(offset);
+ ASSERT_EQ(0, *offset);
+
+ auto events = poller.ReadQueue();
+ ASSERT_EQ(1u, events.size());
+ auto data = events[0].GetTimeSyncEventData();
+ ASSERT_TRUE(data);
+ ASSERT_TRUE(data->valid);
+ ASSERT_EQ(0, data->serverTimeOffset);
+ ASSERT_EQ(0, data->rtt2);
+
+ m_inst.StopServer();
+ offset = m_inst.GetServerTimeOffset();
+ ASSERT_FALSE(offset);
+
+ events = poller.ReadQueue();
+ ASSERT_EQ(1u, events.size());
+ data = events[0].GetTimeSyncEventData();
+ ASSERT_TRUE(data);
+ ASSERT_FALSE(data->valid);
+}
+
+TEST_F(TimeSyncTest, TestClient3) {
+ m_inst.StartClient3("client");
+ auto offset = m_inst.GetServerTimeOffset();
+ ASSERT_FALSE(offset);
+
+ m_inst.StopClient();
+ offset = m_inst.GetServerTimeOffset();
+ ASSERT_FALSE(offset);
+}
+
+TEST_F(TimeSyncTest, TestClient4) {
+ m_inst.StartClient4("client");
+ auto offset = m_inst.GetServerTimeOffset();
+ ASSERT_FALSE(offset);
+
+ m_inst.StopClient();
+ offset = m_inst.GetServerTimeOffset();
+ ASSERT_FALSE(offset);
+}
diff --git a/ntcore/src/test/native/cpp/TopicListenerTest.cpp b/ntcore/src/test/native/cpp/TopicListenerTest.cpp
new file mode 100644
index 0000000..f0a002a
--- /dev/null
+++ b/ntcore/src/test/native/cpp/TopicListenerTest.cpp
@@ -0,0 +1,285 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <chrono>
+#include <thread>
+
+#include <wpi/Synchronization.h>
+#include <wpi/json.h>
+
+#include "TestPrinters.h"
+#include "ValueMatcher.h"
+#include "gtest/gtest.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+class TopicListenerTest : public ::testing::Test {
+ public:
+ TopicListenerTest()
+ : m_serverInst(nt::CreateInstance()), m_clientInst(nt::CreateInstance()) {
+#if 0
+ nt::AddLogger(m_serverInst, 0, UINT_MAX, [](auto& event) {
+ if (auto msg = event.GetLogMessage()) {
+ std::fprintf(stderr, "SERVER: %s\n", msg->message.c_str());
+ }
+ });
+ nt::AddLogger(m_clientInst, 0, UINT_MAX, [](auto& event) {
+ if (auto msg = event.GetLogMessage()) {
+ std::fprintf(stderr, "CLIENT: %s\n", msg.message.c_str());
+ }
+ });
+#endif
+ }
+
+ ~TopicListenerTest() override {
+ nt::DestroyInstance(m_serverInst);
+ nt::DestroyInstance(m_clientInst);
+ }
+
+ void Connect(unsigned int port);
+ static void PublishTopics(NT_Inst inst);
+ void CheckEvents(const std::vector<nt::Event>& events, NT_Listener handle,
+ unsigned int flags, std::string_view topicName = "/foo/bar");
+
+ protected:
+ NT_Inst m_serverInst;
+ NT_Inst m_clientInst;
+};
+
+void TopicListenerTest::Connect(unsigned int port) {
+ nt::StartServer(m_serverInst, "topiclistenertest.json", "127.0.0.1", 0, port);
+ nt::StartClient4(m_clientInst, "client");
+ nt::SetServer(m_clientInst, "127.0.0.1", port);
+
+ // Use connection listener to ensure we've connected
+ NT_ListenerPoller poller = nt::CreateListenerPoller(m_clientInst);
+ nt::AddPolledListener(poller, m_clientInst, nt::EventFlags::kConnected);
+ bool timedOut = false;
+ if (!wpi::WaitForObject(poller, 1.0, &timedOut)) {
+ FAIL() << "client didn't connect to server";
+ }
+}
+
+void TopicListenerTest::PublishTopics(NT_Inst inst) {
+ nt::Publish(nt::GetTopic(inst, "/foo/bar"), NT_DOUBLE, "double");
+ nt::Publish(nt::GetTopic(inst, "/foo"), NT_DOUBLE, "double");
+ nt::Publish(nt::GetTopic(inst, "/baz"), NT_DOUBLE, "double");
+}
+
+void TopicListenerTest::CheckEvents(const std::vector<nt::Event>& events,
+ NT_Listener handle, unsigned int flags,
+ std::string_view topicName) {
+ ASSERT_EQ(events.size(), 1u);
+ ASSERT_EQ(events[0].listener, handle);
+ ASSERT_EQ(events[0].flags, flags);
+ auto topicInfo = events[0].GetTopicInfo();
+ ASSERT_TRUE(topicInfo);
+ ASSERT_EQ(topicInfo->topic, nt::GetTopic(m_serverInst, topicName));
+ ASSERT_EQ(topicInfo->name, topicName);
+}
+
+TEST_F(TopicListenerTest, TopicNewLocal) {
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle = nt::AddPolledListener(
+ poller, nt::GetTopic(m_serverInst, "/foo"), nt::EventFlags::kPublish);
+
+ PublishTopics(m_serverInst);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kPublish, "/foo");
+}
+
+TEST_F(TopicListenerTest, DISABLED_TopicNewRemote) {
+ Connect(10010);
+ if (HasFatalFailure()) {
+ return;
+ }
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle = nt::AddPolledListener(
+ poller, nt::GetTopic(m_serverInst, "/foo"), nt::EventFlags::kPublish);
+
+ PublishTopics(m_clientInst);
+
+ nt::Flush(m_clientInst);
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kPublish, "/foo");
+}
+
+TEST_F(TopicListenerTest, TopicPublishImm) {
+ PublishTopics(m_serverInst);
+
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle = nt::AddPolledListener(
+ poller, nt::GetTopic(m_serverInst, "/foo"),
+ nt::EventFlags::kPublish | nt::EventFlags::kImmediate);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle,
+ nt::EventFlags::kPublish | nt::EventFlags::kImmediate, "/foo");
+}
+
+TEST_F(TopicListenerTest, TopicUnpublishPropsImm) {
+ PublishTopics(m_serverInst);
+
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ nt::AddPolledListener(poller, nt::GetTopic(m_serverInst, "/foo"),
+ nt::EventFlags::kUnpublish |
+ nt::EventFlags::kProperties |
+ nt::EventFlags::kImmediate);
+
+ bool timedOut = false;
+ ASSERT_FALSE(wpi::WaitForObject(poller, 0.02, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ ASSERT_TRUE(events.empty());
+}
+
+TEST_F(TopicListenerTest, TopicUnpublishLocal) {
+ auto topic = nt::GetTopic(m_serverInst, "/foo");
+
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle =
+ nt::AddPolledListener(poller, topic, nt::EventFlags::kUnpublish);
+
+ auto pub = nt::Publish(topic, NT_DOUBLE, "double");
+ nt::Unpublish(pub);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kUnpublish, "/foo");
+}
+
+TEST_F(TopicListenerTest, DISABLED_TopicUnpublishRemote) {
+ Connect(10010);
+ if (HasFatalFailure()) {
+ return;
+ }
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle = nt::AddPolledListener(
+ poller, nt::GetTopic(m_serverInst, "/foo"), nt::EventFlags::kUnpublish);
+
+ auto pub =
+ nt::Publish(nt::GetTopic(m_clientInst, "/foo"), NT_DOUBLE, "double");
+ nt::Flush(m_clientInst);
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ nt::Unpublish(pub);
+
+ nt::Flush(m_clientInst);
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kUnpublish, "/foo");
+}
+
+TEST_F(TopicListenerTest, TopicPropertiesLocal) {
+ auto topic = nt::GetTopic(m_serverInst, "/foo");
+
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle =
+ nt::AddPolledListener(poller, topic, nt::EventFlags::kProperties);
+
+ nt::SetTopicProperty(topic, "foo", 5);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kProperties, "/foo");
+}
+
+TEST_F(TopicListenerTest, DISABLED_TopicPropertiesRemote) {
+ Connect(10010);
+ if (HasFatalFailure()) {
+ return;
+ }
+ // the topic needs to actually exist
+ nt::Publish(nt::GetTopic(m_serverInst, "/foo"), NT_BOOLEAN, "boolean");
+
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle = nt::AddPolledListener(
+ poller, nt::GetTopic(m_serverInst, "/foo"), nt::EventFlags::kProperties);
+ nt::FlushLocal(m_serverInst);
+
+ nt::SetTopicProperty(nt::GetTopic(m_clientInst, "/foo"), "foo", 5);
+ nt::Flush(m_clientInst);
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kProperties, "/foo");
+}
+
+TEST_F(TopicListenerTest, PrefixPublishLocal) {
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle =
+ nt::AddPolledListener(poller, {{"/foo/"}}, nt::EventFlags::kPublish);
+
+ PublishTopics(m_serverInst);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kPublish);
+}
+
+TEST_F(TopicListenerTest, DISABLED_PrefixPublishRemote) {
+ Connect(10011);
+ if (HasFatalFailure()) {
+ return;
+ }
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle =
+ nt::AddPolledListener(poller, {{"/foo/"}}, nt::EventFlags::kPublish);
+
+ PublishTopics(m_clientInst);
+
+ nt::Flush(m_clientInst);
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle, nt::EventFlags::kPublish);
+}
+
+TEST_F(TopicListenerTest, PrefixPublishImm) {
+ PublishTopics(m_serverInst);
+
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ auto handle = nt::AddPolledListener(
+ poller, {{"/foo/"}},
+ nt::EventFlags::kPublish | nt::EventFlags::kImmediate);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ CheckEvents(events, handle,
+ nt::EventFlags::kPublish | nt::EventFlags::kImmediate);
+}
+
+TEST_F(TopicListenerTest, PrefixUnpublishPropsImm) {
+ PublishTopics(m_serverInst);
+
+ auto poller = nt::CreateListenerPoller(m_serverInst);
+ nt::AddPolledListener(poller, {{"/foo/"}},
+ nt::EventFlags::kUnpublish |
+ nt::EventFlags::kProperties |
+ nt::EventFlags::kImmediate);
+
+ bool timedOut = false;
+ ASSERT_FALSE(wpi::WaitForObject(poller, 0.02, &timedOut));
+ auto events = nt::ReadListenerQueue(poller);
+ ASSERT_TRUE(events.empty());
+}
diff --git a/ntcore/src/test/native/cpp/ValueListenerTest.cpp b/ntcore/src/test/native/cpp/ValueListenerTest.cpp
new file mode 100644
index 0000000..dbe3201
--- /dev/null
+++ b/ntcore/src/test/native/cpp/ValueListenerTest.cpp
@@ -0,0 +1,409 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <wpi/StringExtras.h>
+#include <wpi/Synchronization.h>
+
+#include "TestPrinters.h"
+#include "ValueMatcher.h"
+#include "gtest/gtest.h"
+#include "ntcore_c.h"
+#include "ntcore_cpp.h"
+
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::IsNull;
+using ::testing::Return;
+
+namespace nt {
+
+// Test only local here; it's more reliable to mock the network
+class ValueListenerTest : public ::testing::Test {
+ public:
+ ValueListenerTest() : m_inst{nt::CreateInstance()} {}
+
+ ~ValueListenerTest() override { nt::DestroyInstance(m_inst); }
+
+ protected:
+ NT_Inst m_inst;
+};
+
+TEST_F(ValueListenerTest, MultiPollSub) {
+ auto topic = nt::GetTopic(m_inst, "foo");
+ auto pub = nt::Publish(topic, NT_DOUBLE, "double");
+ auto sub = nt::Subscribe(topic, NT_DOUBLE, "double");
+
+ auto poller1 = nt::CreateListenerPoller(m_inst);
+ auto poller2 = nt::CreateListenerPoller(m_inst);
+ auto poller3 = nt::CreateListenerPoller(m_inst);
+ auto h1 = nt::AddPolledListener(poller1, sub, nt::EventFlags::kValueLocal);
+ auto h2 = nt::AddPolledListener(poller2, sub, nt::EventFlags::kValueLocal);
+ auto h3 = nt::AddPolledListener(poller3, sub, nt::EventFlags::kValueLocal);
+
+ nt::SetDouble(pub, 0);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller1, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ ASSERT_TRUE(wpi::WaitForObject(poller2, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ ASSERT_TRUE(wpi::WaitForObject(poller3, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results1 = nt::ReadListenerQueue(poller1);
+ auto results2 = nt::ReadListenerQueue(poller2);
+ auto results3 = nt::ReadListenerQueue(poller3);
+
+ ASSERT_EQ(results1.size(), 1u);
+ EXPECT_EQ(results1[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results1[0].listener, h1);
+ auto valueData = results1[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub);
+ EXPECT_EQ(valueData->topic, topic);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+
+ ASSERT_EQ(results2.size(), 1u);
+ EXPECT_EQ(results2[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results2[0].listener, h2);
+ valueData = results2[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub);
+ EXPECT_EQ(valueData->topic, topic);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+
+ ASSERT_EQ(results3.size(), 1u);
+ EXPECT_EQ(results3[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results3[0].listener, h3);
+ valueData = results3[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub);
+ EXPECT_EQ(valueData->topic, topic);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+}
+
+TEST_F(ValueListenerTest, PollMultiSub) {
+ auto topic = nt::GetTopic(m_inst, "foo");
+ auto pub = nt::Publish(topic, NT_DOUBLE, "double");
+ auto sub1 = nt::Subscribe(topic, NT_DOUBLE, "double");
+ auto sub2 = nt::Subscribe(topic, NT_DOUBLE, "double");
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h1 = nt::AddPolledListener(poller, sub1, nt::EventFlags::kValueLocal);
+ auto h2 = nt::AddPolledListener(poller, sub2, nt::EventFlags::kValueLocal);
+
+ nt::SetDouble(pub, 0);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 2u);
+ EXPECT_EQ(results[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h1);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub1);
+ EXPECT_EQ(valueData->topic, topic);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+
+ EXPECT_EQ(results[1].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[1].listener, h2);
+ valueData = results[1].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub2);
+ EXPECT_EQ(valueData->topic, topic);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+}
+
+TEST_F(ValueListenerTest, PollMultiSubTopic) {
+ auto topic1 = nt::GetTopic(m_inst, "foo");
+ auto topic2 = nt::GetTopic(m_inst, "bar");
+ auto pub1 = nt::Publish(topic1, NT_DOUBLE, "double");
+ auto pub2 = nt::Publish(topic2, NT_DOUBLE, "double");
+ auto sub1 = nt::Subscribe(topic1, NT_DOUBLE, "double");
+ auto sub2 = nt::Subscribe(topic2, NT_DOUBLE, "double");
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h1 = nt::AddPolledListener(poller, sub1, nt::EventFlags::kValueLocal);
+ auto h2 = nt::AddPolledListener(poller, sub2, nt::EventFlags::kValueLocal);
+
+ nt::SetDouble(pub1, 0);
+ nt::SetDouble(pub2, 1);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 2u);
+ EXPECT_EQ(results[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h1);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub1);
+ EXPECT_EQ(valueData->topic, topic1);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+
+ EXPECT_EQ(results[1].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[1].listener, h2);
+ valueData = results[1].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub2);
+ EXPECT_EQ(valueData->topic, topic2);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(1.0));
+}
+
+TEST_F(ValueListenerTest, PollSubMultiple) {
+ auto topic1 = nt::GetTopic(m_inst, "foo/1");
+ auto topic2 = nt::GetTopic(m_inst, "foo/2");
+ auto pub1 = nt::Publish(topic1, NT_DOUBLE, "double");
+ auto pub2 = nt::Publish(topic2, NT_DOUBLE, "double");
+ auto sub = nt::SubscribeMultiple(m_inst, {{"foo"}});
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h = nt::AddPolledListener(poller, sub, nt::EventFlags::kValueLocal);
+
+ nt::SetDouble(pub1, 0);
+ nt::SetDouble(pub2, 1);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 2u);
+ EXPECT_EQ(results[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub);
+ EXPECT_EQ(valueData->topic, topic1);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+
+ EXPECT_EQ(results[1].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[1].listener, h);
+ valueData = results[1].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub);
+ EXPECT_EQ(valueData->topic, topic2);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(1.0));
+}
+
+TEST_F(ValueListenerTest, PollSubPrefixCreated) {
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h =
+ nt::AddPolledListener(poller, {{"foo"}}, nt::EventFlags::kValueLocal);
+
+ auto topic1 = nt::GetTopic(m_inst, "foo/1");
+ auto topic2 = nt::GetTopic(m_inst, "foo/2");
+ auto topic3 = nt::GetTopic(m_inst, "bar/3");
+ auto pub1 = nt::Publish(topic1, NT_DOUBLE, "double");
+ auto pub2 = nt::Publish(topic2, NT_DOUBLE, "double");
+ auto pub3 = nt::Publish(topic3, NT_DOUBLE, "double");
+
+ nt::SetDouble(pub1, 0);
+ nt::SetDouble(pub2, 1);
+ nt::SetDouble(pub3, 1);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 2u);
+ EXPECT_EQ(results[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->topic, topic1);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+
+ EXPECT_EQ(results[1].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[1].listener, h);
+ valueData = results[1].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->topic, topic2);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(1.0));
+}
+
+TEST_F(ValueListenerTest, PollEntry) {
+ auto entry = nt::GetEntry(m_inst, "foo");
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h = nt::AddPolledListener(poller, entry, nt::EventFlags::kValueLocal);
+
+ ASSERT_TRUE(nt::SetDouble(entry, 0));
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 1u);
+ EXPECT_EQ(results[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, entry);
+ EXPECT_EQ(valueData->topic, nt::GetTopic(m_inst, "foo"));
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+}
+
+TEST_F(ValueListenerTest, PollImmediate) {
+ auto entry = nt::GetEntry(m_inst, "foo");
+ ASSERT_TRUE(nt::SetDouble(entry, 0));
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h = nt::AddPolledListener(
+ poller, entry, nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 1u);
+ EXPECT_EQ(results[0].flags &
+ (nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate),
+ nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, entry);
+ EXPECT_EQ(valueData->topic, nt::GetTopic(m_inst, "foo"));
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+}
+
+TEST_F(ValueListenerTest, PollImmediateNoValue) {
+ auto entry = nt::GetEntry(m_inst, "foo");
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h = nt::AddPolledListener(
+ poller, entry, nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate);
+
+ bool timedOut = false;
+ ASSERT_FALSE(wpi::WaitForObject(poller, 0.02, &timedOut));
+ ASSERT_TRUE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+ ASSERT_TRUE(results.empty());
+
+ // now set a value
+ ASSERT_TRUE(nt::SetDouble(entry, 0));
+
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ results = nt::ReadListenerQueue(poller);
+ ASSERT_FALSE(timedOut);
+
+ ASSERT_EQ(results.size(), 1u);
+ EXPECT_EQ(results[0].flags, nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, entry);
+ EXPECT_EQ(valueData->topic, nt::GetTopic(m_inst, "foo"));
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+}
+
+TEST_F(ValueListenerTest, PollImmediateSubMultiple) {
+ auto topic1 = nt::GetTopic(m_inst, "foo/1");
+ auto topic2 = nt::GetTopic(m_inst, "foo/2");
+ auto pub1 = nt::Publish(topic1, NT_DOUBLE, "double");
+ auto pub2 = nt::Publish(topic2, NT_DOUBLE, "double");
+ auto sub = nt::SubscribeMultiple(m_inst, {{"foo"}});
+ nt::SetDouble(pub1, 0);
+ nt::SetDouble(pub2, 1);
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h = nt::AddPolledListener(
+ poller, sub, nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 2u);
+ EXPECT_EQ(results[0].flags &
+ (nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate),
+ nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub);
+ EXPECT_EQ(valueData->topic, topic1);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+
+ EXPECT_EQ(results[1].flags &
+ (nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate),
+ nt::EventFlags::kValueLocal | nt::EventFlags::kImmediate);
+ EXPECT_EQ(results[1].listener, h);
+ valueData = results[1].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub);
+ EXPECT_EQ(valueData->topic, topic2);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(1.0));
+}
+
+TEST_F(ValueListenerTest, TwoSubOneListener) {
+ auto topic = nt::GetTopic(m_inst, "foo");
+ auto pub = nt::Publish(topic, NT_DOUBLE, "double");
+ auto sub1 = nt::Subscribe(topic, NT_DOUBLE, "double");
+ auto sub2 = nt::Subscribe(topic, NT_DOUBLE, "double");
+ auto sub3 = nt::SubscribeMultiple(m_inst, {{"foo"}});
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h = nt::AddPolledListener(poller, sub1, nt::EventFlags::kValueLocal);
+ (void)sub2;
+ (void)sub3;
+
+ nt::SetDouble(pub, 0);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 1u);
+ EXPECT_EQ(results[0].flags & nt::EventFlags::kValueLocal,
+ nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub1);
+ EXPECT_EQ(valueData->topic, topic);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+}
+
+TEST_F(ValueListenerTest, TwoSubOneMultiListener) {
+ auto topic = nt::GetTopic(m_inst, "foo");
+ auto pub = nt::Publish(topic, NT_DOUBLE, "double");
+ auto sub1 = nt::Subscribe(topic, NT_DOUBLE, "double");
+ auto sub2 = nt::Subscribe(topic, NT_DOUBLE, "double");
+ auto sub3 = nt::SubscribeMultiple(m_inst, {{"foo"}});
+
+ auto poller = nt::CreateListenerPoller(m_inst);
+ auto h = nt::AddPolledListener(poller, sub3, nt::EventFlags::kValueLocal);
+ (void)sub1;
+ (void)sub2;
+
+ nt::SetDouble(pub, 0);
+
+ bool timedOut = false;
+ ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut));
+ ASSERT_FALSE(timedOut);
+ auto results = nt::ReadListenerQueue(poller);
+
+ ASSERT_EQ(results.size(), 1u);
+ EXPECT_EQ(results[0].flags & nt::EventFlags::kValueLocal,
+ nt::EventFlags::kValueLocal);
+ EXPECT_EQ(results[0].listener, h);
+ auto valueData = results[0].GetValueEventData();
+ ASSERT_TRUE(valueData);
+ EXPECT_EQ(valueData->subentry, sub3);
+ EXPECT_EQ(valueData->topic, topic);
+ EXPECT_EQ(valueData->value, nt::Value::MakeDouble(0.0));
+}
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/ValueMatcher.cpp b/ntcore/src/test/native/cpp/ValueMatcher.cpp
index 1f55c01..7a0d453 100644
--- a/ntcore/src/test/native/cpp/ValueMatcher.cpp
+++ b/ntcore/src/test/native/cpp/ValueMatcher.cpp
@@ -9,10 +9,9 @@
namespace nt {
bool ValueMatcher::MatchAndExplain(
- std::shared_ptr<Value> val,
- ::testing::MatchResultListener* listener) const {
+ Value val, ::testing::MatchResultListener* listener) const {
if ((!val && goodval) || (val && !goodval) ||
- (val && goodval && *val != *goodval)) {
+ (val && goodval && val != goodval)) {
return false;
}
return true;
diff --git a/ntcore/src/test/native/cpp/ValueMatcher.h b/ntcore/src/test/native/cpp/ValueMatcher.h
index ab68448..c77e04d 100644
--- a/ntcore/src/test/native/cpp/ValueMatcher.h
+++ b/ntcore/src/test/native/cpp/ValueMatcher.h
@@ -2,8 +2,7 @@
// 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.
-#ifndef NTCORE_VALUEMATCHER_H_
-#define NTCORE_VALUEMATCHER_H_
+#pragma once
#include <memory>
#include <ostream>
@@ -14,26 +13,21 @@
namespace nt {
-class ValueMatcher
- : public ::testing::MatcherInterface<std::shared_ptr<Value>> {
+class ValueMatcher : public ::testing::MatcherInterface<Value> {
public:
- explicit ValueMatcher(std::shared_ptr<Value> goodval_)
- : goodval(std::move(goodval_)) {}
+ explicit ValueMatcher(Value goodval_) : goodval(std::move(goodval_)) {}
- bool MatchAndExplain(std::shared_ptr<Value> msg,
+ bool MatchAndExplain(Value msg,
::testing::MatchResultListener* listener) const override;
void DescribeTo(::std::ostream* os) const override;
void DescribeNegationTo(::std::ostream* os) const override;
private:
- std::shared_ptr<Value> goodval;
+ Value goodval;
};
-inline ::testing::Matcher<std::shared_ptr<Value>> ValueEq(
- std::shared_ptr<Value> goodval) {
+inline ::testing::Matcher<Value> ValueEq(const Value& goodval) {
return ::testing::MakeMatcher(new ValueMatcher(goodval));
}
} // namespace nt
-
-#endif // NTCORE_VALUEMATCHER_H_
diff --git a/ntcore/src/test/native/cpp/ValueTest.cpp b/ntcore/src/test/native/cpp/ValueTest.cpp
index a9f2218..d49be44 100644
--- a/ntcore/src/test/native/cpp/ValueTest.cpp
+++ b/ntcore/src/test/native/cpp/ValueTest.cpp
@@ -12,15 +12,15 @@
using namespace std::string_view_literals;
-namespace wpi {
-template <typename T>
-inline bool operator==(span<T> lhs, span<T> rhs) {
+namespace std { // NOLINT (clang-tidy.cert-dcl58-cpp)
+template <typename T, typename U>
+inline bool operator==(std::span<T> lhs, std::span<U> rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
return std::equal(lhs.begin(), lhs.end(), rhs.begin());
}
-} // namespace wpi
+} // namespace std
namespace nt {
@@ -35,19 +35,19 @@
TEST_F(ValueTest, Boolean) {
auto v = Value::MakeBoolean(false);
- ASSERT_EQ(NT_BOOLEAN, v->type());
- ASSERT_FALSE(v->GetBoolean());
+ ASSERT_EQ(NT_BOOLEAN, v.type());
+ ASSERT_FALSE(v.GetBoolean());
NT_Value cv;
NT_InitValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_BOOLEAN, cv.type);
ASSERT_EQ(0, cv.data.v_boolean);
v = Value::MakeBoolean(true);
- ASSERT_EQ(NT_BOOLEAN, v->type());
- ASSERT_TRUE(v->GetBoolean());
+ ASSERT_EQ(NT_BOOLEAN, v.type());
+ ASSERT_TRUE(v.GetBoolean());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_BOOLEAN, cv.type);
ASSERT_EQ(1, cv.data.v_boolean);
@@ -56,19 +56,19 @@
TEST_F(ValueTest, Double) {
auto v = Value::MakeDouble(0.5);
- ASSERT_EQ(NT_DOUBLE, v->type());
- ASSERT_EQ(0.5, v->GetDouble());
+ ASSERT_EQ(NT_DOUBLE, v.type());
+ ASSERT_EQ(0.5, v.GetDouble());
NT_Value cv;
NT_InitValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_DOUBLE, cv.type);
ASSERT_EQ(0.5, cv.data.v_double);
v = Value::MakeDouble(0.25);
- ASSERT_EQ(NT_DOUBLE, v->type());
- ASSERT_EQ(0.25, v->GetDouble());
+ ASSERT_EQ(NT_DOUBLE, v.type());
+ ASSERT_EQ(0.25, v.GetDouble());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_DOUBLE, cv.type);
ASSERT_EQ(0.25, cv.data.v_double);
@@ -77,20 +77,20 @@
TEST_F(ValueTest, String) {
auto v = Value::MakeString("hello");
- ASSERT_EQ(NT_STRING, v->type());
- ASSERT_EQ("hello", v->GetString());
+ ASSERT_EQ(NT_STRING, v.type());
+ ASSERT_EQ("hello", v.GetString());
NT_Value cv;
NT_InitValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_STRING, cv.type);
ASSERT_EQ("hello"sv, cv.data.v_string.str);
ASSERT_EQ(5u, cv.data.v_string.len);
v = Value::MakeString("goodbye");
- ASSERT_EQ(NT_STRING, v->type());
- ASSERT_EQ("goodbye", v->GetString());
+ ASSERT_EQ(NT_STRING, v.type());
+ ASSERT_EQ("goodbye", v.GetString());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_STRING, cv.type);
ASSERT_EQ("goodbye"sv, cv.data.v_string.str);
ASSERT_EQ(7u, cv.data.v_string.len);
@@ -99,24 +99,28 @@
}
TEST_F(ValueTest, Raw) {
- auto v = Value::MakeRaw("hello");
- ASSERT_EQ(NT_RAW, v->type());
- ASSERT_EQ("hello", v->GetRaw());
+ std::vector<uint8_t> arr{5, 4, 3, 2, 1};
+ auto v = Value::MakeRaw(arr);
+ ASSERT_EQ(NT_RAW, v.type());
+ ASSERT_EQ(std::span<const uint8_t>(arr), v.GetRaw());
NT_Value cv;
NT_InitValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_RAW, cv.type);
- ASSERT_EQ("hello"sv, cv.data.v_string.str);
- ASSERT_EQ(5u, cv.data.v_string.len);
+ ASSERT_EQ(5u, cv.data.v_raw.size);
+ ASSERT_EQ(std::span(reinterpret_cast<const uint8_t*>("\5\4\3\2\1"), 5),
+ std::span(cv.data.v_raw.data, 5));
- v = Value::MakeRaw("goodbye");
- ASSERT_EQ(NT_RAW, v->type());
- ASSERT_EQ("goodbye", v->GetRaw());
+ std::vector<uint8_t> arr2{1, 2, 3, 4, 5, 6};
+ v = Value::MakeRaw(arr2);
+ ASSERT_EQ(NT_RAW, v.type());
+ ASSERT_EQ(std::span<const uint8_t>(arr2), v.GetRaw());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_RAW, cv.type);
- ASSERT_EQ("goodbye"sv, cv.data.v_string.str);
- ASSERT_EQ(7u, cv.data.v_string.len);
+ ASSERT_EQ(6u, cv.data.v_raw.size);
+ ASSERT_EQ(std::span(reinterpret_cast<const uint8_t*>("\1\2\3\4\5\6"), 6),
+ std::span(cv.data.v_raw.data, 6));
NT_DisposeValue(&cv);
}
@@ -124,11 +128,11 @@
TEST_F(ValueTest, BooleanArray) {
std::vector<int> vec{1, 0, 1};
auto v = Value::MakeBooleanArray(vec);
- ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type());
- ASSERT_EQ(wpi::span<const int>(vec), v->GetBooleanArray());
+ ASSERT_EQ(NT_BOOLEAN_ARRAY, v.type());
+ ASSERT_EQ(std::span<const int>(vec), v.GetBooleanArray());
NT_Value cv;
NT_InitValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_BOOLEAN_ARRAY, cv.type);
ASSERT_EQ(3u, cv.data.arr_boolean.size);
ASSERT_EQ(vec[0], cv.data.arr_boolean.arr[0]);
@@ -138,10 +142,10 @@
// assign with same size
vec = {0, 1, 0};
v = Value::MakeBooleanArray(vec);
- ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type());
- ASSERT_EQ(wpi::span<const int>(vec), v->GetBooleanArray());
+ ASSERT_EQ(NT_BOOLEAN_ARRAY, v.type());
+ ASSERT_EQ(std::span<const int>(vec), v.GetBooleanArray());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_BOOLEAN_ARRAY, cv.type);
ASSERT_EQ(3u, cv.data.arr_boolean.size);
ASSERT_EQ(vec[0], cv.data.arr_boolean.arr[0]);
@@ -151,10 +155,10 @@
// assign with different size
vec = {1, 0};
v = Value::MakeBooleanArray(vec);
- ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type());
- ASSERT_EQ(wpi::span<const int>(vec), v->GetBooleanArray());
+ ASSERT_EQ(NT_BOOLEAN_ARRAY, v.type());
+ ASSERT_EQ(std::span<const int>(vec), v.GetBooleanArray());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_BOOLEAN_ARRAY, cv.type);
ASSERT_EQ(2u, cv.data.arr_boolean.size);
ASSERT_EQ(vec[0], cv.data.arr_boolean.arr[0]);
@@ -166,11 +170,11 @@
TEST_F(ValueTest, DoubleArray) {
std::vector<double> vec{0.5, 0.25, 0.5};
auto v = Value::MakeDoubleArray(vec);
- ASSERT_EQ(NT_DOUBLE_ARRAY, v->type());
- ASSERT_EQ(wpi::span<const double>(vec), v->GetDoubleArray());
+ ASSERT_EQ(NT_DOUBLE_ARRAY, v.type());
+ ASSERT_EQ(std::span<const double>(vec), v.GetDoubleArray());
NT_Value cv;
NT_InitValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_DOUBLE_ARRAY, cv.type);
ASSERT_EQ(3u, cv.data.arr_double.size);
ASSERT_EQ(vec[0], cv.data.arr_double.arr[0]);
@@ -180,10 +184,10 @@
// assign with same size
vec = {0.25, 0.5, 0.25};
v = Value::MakeDoubleArray(vec);
- ASSERT_EQ(NT_DOUBLE_ARRAY, v->type());
- ASSERT_EQ(wpi::span<const double>(vec), v->GetDoubleArray());
+ ASSERT_EQ(NT_DOUBLE_ARRAY, v.type());
+ ASSERT_EQ(std::span<const double>(vec), v.GetDoubleArray());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_DOUBLE_ARRAY, cv.type);
ASSERT_EQ(3u, cv.data.arr_double.size);
ASSERT_EQ(vec[0], cv.data.arr_double.arr[0]);
@@ -193,10 +197,10 @@
// assign with different size
vec = {0.5, 0.25};
v = Value::MakeDoubleArray(vec);
- ASSERT_EQ(NT_DOUBLE_ARRAY, v->type());
- ASSERT_EQ(wpi::span<const double>(vec), v->GetDoubleArray());
+ ASSERT_EQ(NT_DOUBLE_ARRAY, v.type());
+ ASSERT_EQ(std::span<const double>(vec), v.GetDoubleArray());
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_DOUBLE_ARRAY, cv.type);
ASSERT_EQ(2u, cv.data.arr_double.size);
ASSERT_EQ(vec[0], cv.data.arr_double.arr[0]);
@@ -211,14 +215,14 @@
vec.push_back("goodbye");
vec.push_back("string");
auto v = Value::MakeStringArray(std::move(vec));
- ASSERT_EQ(NT_STRING_ARRAY, v->type());
- ASSERT_EQ(3u, v->GetStringArray().size());
- ASSERT_EQ("hello"sv, v->GetStringArray()[0]);
- ASSERT_EQ("goodbye"sv, v->GetStringArray()[1]);
- ASSERT_EQ("string"sv, v->GetStringArray()[2]);
+ ASSERT_EQ(NT_STRING_ARRAY, v.type());
+ ASSERT_EQ(3u, v.GetStringArray().size());
+ ASSERT_EQ("hello"sv, v.GetStringArray()[0]);
+ ASSERT_EQ("goodbye"sv, v.GetStringArray()[1]);
+ ASSERT_EQ("string"sv, v.GetStringArray()[2]);
NT_Value cv;
NT_InitValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_STRING_ARRAY, cv.type);
ASSERT_EQ(3u, cv.data.arr_string.size);
ASSERT_EQ("hello"sv, cv.data.arr_string.arr[0].str);
@@ -231,13 +235,13 @@
vec.push_back("str2");
vec.push_back("string3");
v = Value::MakeStringArray(vec);
- ASSERT_EQ(NT_STRING_ARRAY, v->type());
- ASSERT_EQ(3u, v->GetStringArray().size());
- ASSERT_EQ("s1"sv, v->GetStringArray()[0]);
- ASSERT_EQ("str2"sv, v->GetStringArray()[1]);
- ASSERT_EQ("string3"sv, v->GetStringArray()[2]);
+ ASSERT_EQ(NT_STRING_ARRAY, v.type());
+ ASSERT_EQ(3u, v.GetStringArray().size());
+ ASSERT_EQ("s1"sv, v.GetStringArray()[0]);
+ ASSERT_EQ("str2"sv, v.GetStringArray()[1]);
+ ASSERT_EQ("string3"sv, v.GetStringArray()[2]);
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_STRING_ARRAY, cv.type);
ASSERT_EQ(3u, cv.data.arr_string.size);
ASSERT_EQ("s1"sv, cv.data.arr_string.arr[0].str);
@@ -249,12 +253,12 @@
vec.push_back("short");
vec.push_back("er");
v = Value::MakeStringArray(std::move(vec));
- ASSERT_EQ(NT_STRING_ARRAY, v->type());
- ASSERT_EQ(2u, v->GetStringArray().size());
- ASSERT_EQ("short"sv, v->GetStringArray()[0]);
- ASSERT_EQ("er"sv, v->GetStringArray()[1]);
+ ASSERT_EQ(NT_STRING_ARRAY, v.type());
+ ASSERT_EQ(2u, v.GetStringArray().size());
+ ASSERT_EQ("short"sv, v.GetStringArray()[0]);
+ ASSERT_EQ("er"sv, v.GetStringArray()[1]);
NT_DisposeValue(&cv);
- ConvertToC(*v, &cv);
+ ConvertToC(v, &cv);
ASSERT_EQ(NT_STRING_ARRAY, cv.type);
ASSERT_EQ(2u, cv.data.arr_string.size);
ASSERT_EQ("short"sv, cv.data.arr_string.arr[0].str);
@@ -286,69 +290,69 @@
TEST_F(ValueTest, MixedComparison) {
Value v1;
auto v2 = Value::MakeBoolean(true);
- ASSERT_NE(v1, *v2); // unassigned vs boolean
+ ASSERT_NE(v1, v2); // unassigned vs boolean
auto v3 = Value::MakeDouble(0.5);
- ASSERT_NE(*v2, *v3); // boolean vs double
+ ASSERT_NE(v2, v3); // boolean vs double
}
TEST_F(ValueTest, BooleanComparison) {
auto v1 = Value::MakeBoolean(true);
auto v2 = Value::MakeBoolean(true);
- ASSERT_EQ(*v1, *v2);
+ ASSERT_EQ(v1, v2);
v2 = Value::MakeBoolean(false);
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
}
TEST_F(ValueTest, DoubleComparison) {
auto v1 = Value::MakeDouble(0.25);
auto v2 = Value::MakeDouble(0.25);
- ASSERT_EQ(*v1, *v2);
+ ASSERT_EQ(v1, v2);
v2 = Value::MakeDouble(0.5);
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
}
TEST_F(ValueTest, StringComparison) {
auto v1 = Value::MakeString("hello");
auto v2 = Value::MakeString("hello");
- ASSERT_EQ(*v1, *v2);
+ ASSERT_EQ(v1, v2);
v2 = Value::MakeString("world"); // different contents
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
v2 = Value::MakeString("goodbye"); // different size
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
}
TEST_F(ValueTest, BooleanArrayComparison) {
std::vector<int> vec{1, 0, 1};
auto v1 = Value::MakeBooleanArray(vec);
auto v2 = Value::MakeBooleanArray(vec);
- ASSERT_EQ(*v1, *v2);
+ ASSERT_EQ(v1, v2);
// different contents
vec = {1, 1, 1};
v2 = Value::MakeBooleanArray(vec);
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
// different size
vec = {1, 0};
v2 = Value::MakeBooleanArray(vec);
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
}
TEST_F(ValueTest, DoubleArrayComparison) {
std::vector<double> vec{0.5, 0.25, 0.5};
auto v1 = Value::MakeDoubleArray(vec);
auto v2 = Value::MakeDoubleArray(vec);
- ASSERT_EQ(*v1, *v2);
+ ASSERT_EQ(v1, v2);
// different contents
vec = {0.5, 0.5, 0.5};
v2 = Value::MakeDoubleArray(vec);
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
// different size
vec = {0.5, 0.25};
v2 = Value::MakeDoubleArray(vec);
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
}
TEST_F(ValueTest, StringArrayComparison) {
@@ -362,7 +366,7 @@
vec.push_back("goodbye");
vec.push_back("string");
auto v2 = Value::MakeStringArray(std::move(vec));
- ASSERT_EQ(*v1, *v2);
+ ASSERT_EQ(v1, v2);
// different contents
vec.clear();
@@ -370,7 +374,7 @@
vec.push_back("goodby2");
vec.push_back("string");
v2 = Value::MakeStringArray(std::move(vec));
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
// different sized contents
vec.clear();
@@ -378,14 +382,14 @@
vec.push_back("goodbye2");
vec.push_back("string");
v2 = Value::MakeStringArray(vec);
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
// different size
vec.clear();
vec.push_back("hello");
vec.push_back("goodbye");
v2 = Value::MakeStringArray(std::move(vec));
- ASSERT_NE(*v1, *v2);
+ ASSERT_NE(v1, v2);
}
} // namespace nt
diff --git a/ntcore/src/test/native/cpp/WireDecoderTest.cpp b/ntcore/src/test/native/cpp/WireDecoderTest.cpp
deleted file mode 100644
index 313fd98..0000000
--- a/ntcore/src/test/native/cpp/WireDecoderTest.cpp
+++ /dev/null
@@ -1,647 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include <stdint.h>
-
-#include <cfloat>
-#include <climits>
-#include <string>
-#include <string_view>
-
-#include "TestPrinters.h"
-#include "WireDecoder.h"
-#include "gtest/gtest.h"
-
-using namespace std::string_view_literals;
-
-namespace nt {
-
-class WireDecoderTest : public ::testing::Test {
- protected:
- WireDecoderTest() {
- v_boolean = Value::MakeBoolean(true);
- v_double = Value::MakeDouble(1.0);
- v_string = Value::MakeString("hello"sv);
- v_raw = Value::MakeRaw("hello"sv);
- v_boolean_array = Value::MakeBooleanArray(std::vector<int>{0, 1, 0});
- v_boolean_array_big = Value::MakeBooleanArray(std::vector<int>(255));
- v_double_array = Value::MakeDoubleArray(std::vector<double>{0.5, 0.25});
- v_double_array_big = Value::MakeDoubleArray(std::vector<double>(255));
-
- std::vector<std::string> sa;
- sa.push_back("hello");
- sa.push_back("goodbye");
- v_string_array = Value::MakeStringArray(std::move(sa));
-
- sa.clear();
- for (int i = 0; i < 255; ++i) {
- sa.push_back("h");
- }
- v_string_array_big = Value::MakeStringArray(std::move(sa));
-
- s_normal = std::string("hello");
-
- s_long.clear();
- s_long.append(127, '*');
- s_long.push_back('x');
-
- s_big2.clear();
- s_big2.append(65534, '*');
- s_big2.push_back('x');
-
- s_big3.clear();
- s_big3.append(65534, '*');
- s_big3.append(3, 'x');
- }
-
- std::shared_ptr<Value> v_boolean, v_double, v_string, v_raw;
- std::shared_ptr<Value> v_boolean_array, v_boolean_array_big;
- std::shared_ptr<Value> v_double_array, v_double_array_big;
- std::shared_ptr<Value> v_string_array, v_string_array_big;
-
- std::string s_normal, s_long, s_big2, s_big3;
-};
-
-TEST_F(WireDecoderTest, Construct) {
- wpi::raw_mem_istream is("", 0);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- EXPECT_EQ(nullptr, d.error());
- EXPECT_EQ(0x0300u, d.proto_rev());
-}
-
-TEST_F(WireDecoderTest, SetProtoRev) {
- wpi::raw_mem_istream is("", 0);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- d.set_proto_rev(0x0200u);
- EXPECT_EQ(0x0200u, d.proto_rev());
-}
-
-TEST_F(WireDecoderTest, Read8) {
- wpi::raw_mem_istream is("\x05\x01\x00", 3);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- unsigned int val;
- ASSERT_TRUE(d.Read8(&val));
- EXPECT_EQ(5u, val);
- ASSERT_TRUE(d.Read8(&val));
- EXPECT_EQ(1u, val);
- ASSERT_TRUE(d.Read8(&val));
- EXPECT_EQ(0u, val);
- ASSERT_FALSE(d.Read8(&val));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, Read16) {
- wpi::raw_mem_istream is("\x00\x05\x00\x01\x45\x67\x00\x00", 8);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- unsigned int val;
- ASSERT_TRUE(d.Read16(&val));
- EXPECT_EQ(5u, val);
- ASSERT_TRUE(d.Read16(&val));
- EXPECT_EQ(1u, val);
- ASSERT_TRUE(d.Read16(&val));
- EXPECT_EQ(0x4567u, val);
- ASSERT_TRUE(d.Read16(&val));
- EXPECT_EQ(0u, val);
- ASSERT_FALSE(d.Read16(&val));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, Read32) {
- wpi::raw_mem_istream is(
- "\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\xab\xcd"
- "\x12\x34\x56\x78\x00\x00\x00\x00",
- 20);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- uint32_t val;
- ASSERT_TRUE(d.Read32(&val));
- EXPECT_EQ(5ul, val);
- ASSERT_TRUE(d.Read32(&val));
- EXPECT_EQ(1ul, val);
- ASSERT_TRUE(d.Read32(&val));
- EXPECT_EQ(0xabcdul, val);
- ASSERT_TRUE(d.Read32(&val));
- EXPECT_EQ(0x12345678ul, val);
- ASSERT_TRUE(d.Read32(&val));
- EXPECT_EQ(0ul, val);
- ASSERT_FALSE(d.Read32(&val));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadDouble) {
- // values except min and max from
- // http://www.binaryconvert.com/result_double.html
- wpi::raw_mem_istream is(
- "\x00\x00\x00\x00\x00\x00\x00\x00"
- "\x41\x0c\x13\x80\x00\x00\x00\x00"
- "\x7f\xf0\x00\x00\x00\x00\x00\x00"
- "\x00\x10\x00\x00\x00\x00\x00\x00"
- "\x7f\xef\xff\xff\xff\xff\xff\xff",
- 40);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- double val;
- ASSERT_TRUE(d.ReadDouble(&val));
- EXPECT_EQ(0.0, val);
- ASSERT_TRUE(d.ReadDouble(&val));
- EXPECT_EQ(2.3e5, val);
- ASSERT_TRUE(d.ReadDouble(&val));
- EXPECT_EQ(std::numeric_limits<double>::infinity(), val);
- ASSERT_TRUE(d.ReadDouble(&val));
- EXPECT_EQ(DBL_MIN, val);
- ASSERT_TRUE(d.ReadDouble(&val));
- EXPECT_EQ(DBL_MAX, val);
- ASSERT_FALSE(d.ReadDouble(&val));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadUleb128) {
- wpi::raw_mem_istream is("\x00\x7f\x80\x01\x80", 5);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- uint64_t val;
- ASSERT_TRUE(d.ReadUleb128(&val));
- EXPECT_EQ(0ul, val);
- ASSERT_TRUE(d.ReadUleb128(&val));
- EXPECT_EQ(0x7ful, val);
- ASSERT_TRUE(d.ReadUleb128(&val));
- EXPECT_EQ(0x80ul, val);
- ASSERT_FALSE(d.ReadUleb128(&val)); // partial
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadType) {
- wpi::raw_mem_istream is("\x00\x01\x02\x03\x10\x11\x12\x20", 8);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- NT_Type val;
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_BOOLEAN, val);
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_DOUBLE, val);
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_STRING, val);
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_RAW, val);
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_BOOLEAN_ARRAY, val);
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_DOUBLE_ARRAY, val);
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_STRING_ARRAY, val);
- ASSERT_TRUE(d.ReadType(&val));
- EXPECT_EQ(NT_RPC, val);
- ASSERT_FALSE(d.ReadType(&val));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadTypeError) {
- wpi::raw_mem_istream is("\x30", 1);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- NT_Type val;
- ASSERT_FALSE(d.ReadType(&val));
- EXPECT_EQ(NT_UNASSIGNED, val);
- ASSERT_NE(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, Reset) {
- wpi::raw_mem_istream is("\x30", 1);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- NT_Type val;
- ASSERT_FALSE(d.ReadType(&val));
- EXPECT_EQ(NT_UNASSIGNED, val);
- ASSERT_NE(nullptr, d.error());
- d.Reset();
- EXPECT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadBooleanValue2) {
- wpi::raw_mem_istream is("\x01\x00", 2);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_BOOLEAN);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean, *val);
-
- auto v_false = Value::MakeBoolean(false);
- val = d.ReadValue(NT_BOOLEAN);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_false, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_BOOLEAN));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadDoubleValue2) {
- wpi::raw_mem_istream is(
- "\x3f\xf0\x00\x00\x00\x00\x00\x00"
- "\x3f\xf0\x00\x00\x00\x00\x00\x00",
- 16);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_DOUBLE);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double, *val);
-
- val = d.ReadValue(NT_DOUBLE);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_DOUBLE));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadStringValue2) {
- wpi::raw_mem_istream is(
- "\x00\x05hello\x00\x03"
- "bye\x55",
- 13);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_STRING);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_string, *val);
-
- auto v_bye = Value::MakeString("bye"sv);
- val = d.ReadValue(NT_STRING);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_bye, *val);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadValue(NT_STRING));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadBooleanArrayValue2) {
- wpi::raw_mem_istream is("\x03\x00\x01\x00\x02\x01\x00\xff", 8);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean_array, *val);
-
- auto v_boolean_array2 = Value::MakeBooleanArray(std::vector<int>{1, 0});
- val = d.ReadValue(NT_BOOLEAN_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean_array2, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadBooleanArrayBigValue2) {
- std::string s;
- s.push_back('\xff');
- s.append(255, '\x00');
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean_array_big, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadDoubleArrayValue2) {
- wpi::raw_mem_istream is(
- "\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
- "\x3f\xd0\x00\x00\x00\x00\x00\x00\x55",
- 18);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_DOUBLE_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double_array, *val);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadDoubleArrayBigValue2) {
- std::string s;
- s.push_back('\xff');
- s.append(255 * 8, '\x00');
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_DOUBLE_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double_array_big, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadStringArrayValue2) {
- wpi::raw_mem_istream is("\x02\x00\x05hello\x00\x07goodbye\x55", 18);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_STRING_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_string_array, *val);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadStringArrayBigValue2) {
- std::string s;
- s.push_back('\xff');
- for (int i = 0; i < 255; ++i) {
- s.append("\x00\x01h", 3);
- }
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- auto val = d.ReadValue(NT_STRING_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_string_array_big, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadValueError2) {
- wpi::raw_mem_istream is("", 0);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- ASSERT_FALSE(d.ReadValue(NT_UNASSIGNED)); // unassigned
- ASSERT_NE(nullptr, d.error());
-
- d.Reset();
- ASSERT_FALSE(d.ReadValue(NT_RAW)); // not supported
- ASSERT_NE(nullptr, d.error());
-
- d.Reset();
- ASSERT_FALSE(d.ReadValue(NT_RPC)); // not supported
- ASSERT_NE(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadBooleanValue3) {
- wpi::raw_mem_istream is("\x01\x00", 2);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_BOOLEAN);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean, *val);
-
- auto v_false = Value::MakeBoolean(false);
- val = d.ReadValue(NT_BOOLEAN);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_false, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_BOOLEAN));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadDoubleValue3) {
- wpi::raw_mem_istream is(
- "\x3f\xf0\x00\x00\x00\x00\x00\x00"
- "\x3f\xf0\x00\x00\x00\x00\x00\x00",
- 16);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_DOUBLE);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double, *val);
-
- val = d.ReadValue(NT_DOUBLE);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_DOUBLE));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadStringValue3) {
- wpi::raw_mem_istream is(
- "\x05hello\x03"
- "bye\x55",
- 11);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_STRING);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_string, *val);
-
- auto v_bye = Value::MakeString("bye"sv);
- val = d.ReadValue(NT_STRING);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_bye, *val);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadValue(NT_STRING));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadRawValue3) {
- wpi::raw_mem_istream is(
- "\x05hello\x03"
- "bye\x55",
- 11);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_RAW);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_raw, *val);
-
- auto v_bye = Value::MakeRaw("bye"sv);
- val = d.ReadValue(NT_RAW);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_bye, *val);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadValue(NT_RAW));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadBooleanArrayValue3) {
- wpi::raw_mem_istream is("\x03\x00\x01\x00\x02\x01\x00\xff", 8);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean_array, *val);
-
- auto v_boolean_array2 = Value::MakeBooleanArray(std::vector<int>{1, 0});
- val = d.ReadValue(NT_BOOLEAN_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean_array2, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadBooleanArrayBigValue3) {
- std::string s;
- s.push_back('\xff');
- s.append(255, '\x00');
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_BOOLEAN_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_boolean_array_big, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_BOOLEAN_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadDoubleArrayValue3) {
- wpi::raw_mem_istream is(
- "\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
- "\x3f\xd0\x00\x00\x00\x00\x00\x00\x55",
- 18);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_DOUBLE_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double_array, *val);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadDoubleArrayBigValue3) {
- std::string s;
- s.push_back('\xff');
- s.append(255 * 8, '\x00');
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_DOUBLE_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_double_array_big, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_DOUBLE_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadStringArrayValue3) {
- wpi::raw_mem_istream is("\x02\x05hello\x07goodbye\x55", 16);
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_STRING_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_string_array, *val);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadStringArrayBigValue3) {
- std::string s;
- s.push_back('\xff');
- for (int i = 0; i < 255; ++i) {
- s.append("\x01h", 2);
- }
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- auto val = d.ReadValue(NT_STRING_ARRAY);
- ASSERT_TRUE(static_cast<bool>(val));
- EXPECT_EQ(*v_string_array_big, *val);
-
- ASSERT_FALSE(d.ReadValue(NT_STRING_ARRAY));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadValueError3) {
- wpi::raw_mem_istream is("", 0);
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- ASSERT_FALSE(d.ReadValue(NT_UNASSIGNED)); // unassigned
- ASSERT_NE(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadString2) {
- std::string s;
- s.append("\x00\x05", 2);
- s += s_normal;
- s.append("\x00\x80", 2);
- s += s_long;
- s.append("\xff\xff", 2);
- s += s_big2;
- s.push_back('\x55');
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0200u, logger);
- std::string outs;
- ASSERT_TRUE(d.ReadString(&outs));
- EXPECT_EQ(s_normal, outs);
- ASSERT_TRUE(d.ReadString(&outs));
- EXPECT_EQ(s_long, outs);
- ASSERT_TRUE(d.ReadString(&outs));
- EXPECT_EQ(s_big2, outs);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadString(&outs));
- ASSERT_EQ(nullptr, d.error());
-}
-
-TEST_F(WireDecoderTest, ReadString3) {
- std::string s;
- s.push_back('\x05');
- s += s_normal;
- s.append("\x80\x01", 2);
- s += s_long;
- s.append("\x81\x80\x04", 3);
- s += s_big3;
- s.push_back('\x55');
- wpi::raw_mem_istream is(s.data(), s.size());
- wpi::Logger logger;
- WireDecoder d(is, 0x0300u, logger);
- std::string outs;
- ASSERT_TRUE(d.ReadString(&outs));
- EXPECT_EQ(s_normal, outs);
- ASSERT_TRUE(d.ReadString(&outs));
- EXPECT_EQ(s_long, outs);
- ASSERT_TRUE(d.ReadString(&outs));
- EXPECT_EQ(s_big3, outs);
-
- unsigned int b;
- ASSERT_TRUE(d.Read8(&b));
- EXPECT_EQ(0x55u, b);
-
- ASSERT_FALSE(d.ReadString(&outs));
- ASSERT_EQ(nullptr, d.error());
-}
-
-} // namespace nt
diff --git a/ntcore/src/test/native/cpp/WireEncoderTest.cpp b/ntcore/src/test/native/cpp/WireEncoderTest.cpp
deleted file mode 100644
index b0cc664..0000000
--- a/ntcore/src/test/native/cpp/WireEncoderTest.cpp
+++ /dev/null
@@ -1,500 +0,0 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
-#include <cfloat>
-#include <climits>
-#include <string>
-#include <string_view>
-
-#include <wpi/StringExtras.h>
-
-#include "TestPrinters.h"
-#include "WireEncoder.h"
-#include "gtest/gtest.h"
-
-#define BUFSIZE 1024
-
-using namespace std::string_view_literals;
-
-namespace nt {
-
-class WireEncoderTest : public ::testing::Test {
- protected:
- WireEncoderTest() {
- v_empty = std::make_shared<Value>();
- v_boolean = Value::MakeBoolean(true);
- v_double = Value::MakeDouble(1.0);
- v_string = Value::MakeString("hello"sv);
- v_raw = Value::MakeRaw("hello"sv);
- v_boolean_array = Value::MakeBooleanArray(std::vector<int>{0, 1, 0});
- v_boolean_array_big = Value::MakeBooleanArray(std::vector<int>(256));
- v_double_array = Value::MakeDoubleArray(std::vector<double>{0.5, 0.25});
- v_double_array_big = Value::MakeDoubleArray(std::vector<double>(256));
-
- std::vector<std::string> sa;
- sa.push_back("hello");
- sa.push_back("goodbye");
- v_string_array = Value::MakeStringArray(std::move(sa));
-
- sa.clear();
- for (int i = 0; i < 256; ++i) {
- sa.push_back("h");
- }
- v_string_array_big = Value::MakeStringArray(std::move(sa));
-
- s_normal = "hello";
-
- s_long.clear();
- s_long.append(127, '*');
- s_long.push_back('x');
-
- s_big.clear();
- s_big.append(65534, '*');
- s_big.append(3, 'x');
- }
-
- std::shared_ptr<Value> v_empty;
- std::shared_ptr<Value> v_boolean, v_double, v_string, v_raw;
- std::shared_ptr<Value> v_boolean_array, v_boolean_array_big;
- std::shared_ptr<Value> v_double_array, v_double_array_big;
- std::shared_ptr<Value> v_string_array, v_string_array_big;
-
- std::string s_normal, s_long, s_big;
-};
-
-TEST_F(WireEncoderTest, Construct) {
- WireEncoder e(0x0300u);
- EXPECT_EQ(0u, e.size());
- EXPECT_EQ(nullptr, e.error());
- EXPECT_EQ(0x0300u, e.proto_rev());
-}
-
-TEST_F(WireEncoderTest, SetProtoRev) {
- WireEncoder e(0x0300u);
- e.set_proto_rev(0x0200u);
- EXPECT_EQ(0x0200u, e.proto_rev());
-}
-
-TEST_F(WireEncoderTest, Write8) {
- size_t off = BUFSIZE - 1;
- WireEncoder e(0x0300u);
- for (size_t i = 0; i < off; ++i) {
- e.Write8(0u); // test across Reserve()
- }
- e.Write8(5u);
- e.Write8(0x101u); // should be truncated
- e.Write8(0u);
- ASSERT_EQ(3u, e.size() - off);
- ASSERT_EQ("\x05\x01\x00"sv, wpi::substr({e.data(), e.size()}, off));
-}
-
-TEST_F(WireEncoderTest, Write16) {
- size_t off = BUFSIZE - 2;
- WireEncoder e(0x0300u);
- for (size_t i = 0; i < off; ++i) {
- e.Write8(0u); // test across Reserve()
- }
- e.Write16(5u);
- e.Write16(0x10001u); // should be truncated
- e.Write16(0x4567u);
- e.Write16(0u);
- ASSERT_EQ(8u, e.size() - off);
- ASSERT_EQ("\x00\x05\x00\x01\x45\x67\x00\x00"sv,
- wpi::substr({e.data(), e.size()}, off));
-}
-
-TEST_F(WireEncoderTest, Write32) {
- size_t off = BUFSIZE - 4;
- WireEncoder e(0x0300u);
- for (size_t i = 0; i < off; ++i) {
- e.Write8(0u); // test across Reserve()
- }
- e.Write32(5ul);
- e.Write32(1ul);
- e.Write32(0xabcdul);
- e.Write32(0x12345678ul);
- e.Write32(0ul);
- ASSERT_EQ(20u, e.size() - off);
- ASSERT_EQ(std::string_view("\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\xab\xcd"
- "\x12\x34\x56\x78\x00\x00\x00\x00",
- 20),
- wpi::substr({e.data(), e.size()}, off));
-}
-
-TEST_F(WireEncoderTest, WriteDouble) {
- size_t off = BUFSIZE - 8;
- WireEncoder e(0x0300u);
- for (size_t i = 0; i < off; ++i) {
- e.Write8(0u); // test across Reserve()
- }
- e.WriteDouble(0.0);
- e.WriteDouble(2.3e5);
- e.WriteDouble(std::numeric_limits<double>::infinity());
- e.WriteDouble(DBL_MIN);
- e.WriteDouble(DBL_MAX);
- ASSERT_EQ(40u, e.size() - off);
- // golden values except min and max from
- // http://www.binaryconvert.com/result_double.html
- ASSERT_EQ(std::string_view("\x00\x00\x00\x00\x00\x00\x00\x00"
- "\x41\x0c\x13\x80\x00\x00\x00\x00"
- "\x7f\xf0\x00\x00\x00\x00\x00\x00"
- "\x00\x10\x00\x00\x00\x00\x00\x00"
- "\x7f\xef\xff\xff\xff\xff\xff\xff",
- 40),
- wpi::substr({e.data(), e.size()}, off));
-}
-
-TEST_F(WireEncoderTest, WriteUleb128) {
- size_t off = BUFSIZE - 2;
- WireEncoder e(0x0300u);
- for (size_t i = 0; i < off; ++i) {
- e.Write8(0u); // test across Reserve()
- }
- e.WriteUleb128(0ul);
- e.WriteUleb128(0x7ful);
- e.WriteUleb128(0x80ul);
- ASSERT_EQ(4u, e.size() - off);
- ASSERT_EQ("\x00\x7f\x80\x01"sv, wpi::substr({e.data(), e.size()}, off));
-}
-
-TEST_F(WireEncoderTest, WriteType) {
- size_t off = BUFSIZE - 1;
- WireEncoder e(0x0300u);
- for (size_t i = 0; i < off; ++i) {
- e.Write8(0u); // test across Reserve()
- }
- e.WriteType(NT_BOOLEAN);
- e.WriteType(NT_DOUBLE);
- e.WriteType(NT_STRING);
- e.WriteType(NT_RAW);
- e.WriteType(NT_BOOLEAN_ARRAY);
- e.WriteType(NT_DOUBLE_ARRAY);
- e.WriteType(NT_STRING_ARRAY);
- e.WriteType(NT_RPC);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(8u, e.size() - off);
- ASSERT_EQ("\x00\x01\x02\x03\x10\x11\x12\x20"sv,
- wpi::substr({e.data(), e.size()}, off));
-}
-
-TEST_F(WireEncoderTest, WriteTypeError) {
- WireEncoder e(0x0200u);
- e.WriteType(NT_UNASSIGNED);
- EXPECT_EQ(0u, e.size());
- EXPECT_EQ(std::string("unrecognized type"), e.error());
-
- e.Reset();
- e.WriteType(NT_RAW);
- EXPECT_EQ(0u, e.size());
- EXPECT_EQ(std::string("raw type not supported in protocol < 3.0"), e.error());
-
- e.Reset();
- e.WriteType(NT_RPC);
- EXPECT_EQ(0u, e.size());
- EXPECT_EQ(std::string("RPC type not supported in protocol < 3.0"), e.error());
-}
-
-TEST_F(WireEncoderTest, Reset) {
- WireEncoder e(0x0300u);
- e.WriteType(NT_UNASSIGNED);
- EXPECT_NE(nullptr, e.error());
- e.Reset();
- EXPECT_EQ(nullptr, e.error());
-
- e.Write8(0u);
- EXPECT_EQ(1u, e.size());
- e.Reset();
- EXPECT_EQ(0u, e.size());
-}
-
-TEST_F(WireEncoderTest, GetValueSize2) {
- WireEncoder e(0x0200u);
- EXPECT_EQ(0u, e.GetValueSize(*v_empty)); // empty
- EXPECT_EQ(1u, e.GetValueSize(*v_boolean));
- EXPECT_EQ(8u, e.GetValueSize(*v_double));
- EXPECT_EQ(7u, e.GetValueSize(*v_string));
- EXPECT_EQ(0u, e.GetValueSize(*v_raw)); // not supported
-
- EXPECT_EQ(1u + 3u, e.GetValueSize(*v_boolean_array));
- // truncated
- EXPECT_EQ(1u + 255u, e.GetValueSize(*v_boolean_array_big));
-
- EXPECT_EQ(1u + 2u * 8u, e.GetValueSize(*v_double_array));
- // truncated
- EXPECT_EQ(1u + 255u * 8u, e.GetValueSize(*v_double_array_big));
-
- EXPECT_EQ(1u + 7u + 9u, e.GetValueSize(*v_string_array));
- // truncated
- EXPECT_EQ(1u + 255u * 3u, e.GetValueSize(*v_string_array_big));
-}
-
-TEST_F(WireEncoderTest, WriteBooleanValue2) {
- WireEncoder e(0x0200u);
- e.WriteValue(*v_boolean);
- auto v_false = Value::MakeBoolean(false);
- e.WriteValue(*v_false);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(2u, e.size());
- ASSERT_EQ("\x01\x00"sv, std::string_view(e.data(), e.size()));
-}
-
-TEST_F(WireEncoderTest, WriteDoubleValue2) {
- WireEncoder e(0x0200u);
- e.WriteValue(*v_double);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(8u, e.size());
- ASSERT_EQ("\x3f\xf0\x00\x00\x00\x00\x00\x00"sv,
- std::string_view(e.data(), e.size()));
-}
-
-TEST_F(WireEncoderTest, WriteStringValue2) {
- WireEncoder e(0x0200u);
- e.WriteValue(*v_string);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(7u, e.size());
- ASSERT_EQ("\x00\x05hello"sv, std::string_view(e.data(), e.size()));
-}
-
-TEST_F(WireEncoderTest, WriteBooleanArrayValue2) {
- WireEncoder e(0x0200u);
- e.WriteValue(*v_boolean_array);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 3u, e.size());
- ASSERT_EQ("\x03\x00\x01\x00"sv, std::string_view(e.data(), e.size()));
-
- // truncated
- e.Reset();
- e.WriteValue(*v_boolean_array_big);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 255u, e.size());
- ASSERT_EQ("\xff\x00"sv, std::string_view(e.data(), 2));
-}
-
-TEST_F(WireEncoderTest, WriteDoubleArrayValue2) {
- WireEncoder e(0x0200u);
- e.WriteValue(*v_double_array);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 2u * 8u, e.size());
- ASSERT_EQ(std::string_view("\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
- "\x3f\xd0\x00\x00\x00\x00\x00\x00",
- 17),
- std::string_view(e.data(), e.size()));
-
- // truncated
- e.Reset();
- e.WriteValue(*v_double_array_big);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 255u * 8u, e.size());
- ASSERT_EQ("\xff\x00"sv, std::string_view(e.data(), 2));
-}
-
-TEST_F(WireEncoderTest, WriteStringArrayValue2) {
- WireEncoder e(0x0200u);
- e.WriteValue(*v_string_array);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 7u + 9u, e.size());
- ASSERT_EQ("\x02\x00\x05hello\x00\x07goodbye"sv,
- std::string_view(e.data(), e.size()));
-
- // truncated
- e.Reset();
- e.WriteValue(*v_string_array_big);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 255u * 3u, e.size());
- ASSERT_EQ("\xff\x00\x01"sv, std::string_view(e.data(), 3));
-}
-
-TEST_F(WireEncoderTest, WriteValueError2) {
- WireEncoder e(0x0200u);
- e.WriteValue(*v_empty); // empty
- ASSERT_EQ(0u, e.size());
- ASSERT_NE(nullptr, e.error());
-
- e.Reset();
- e.WriteValue(*v_raw); // not supported
- ASSERT_EQ(0u, e.size());
- ASSERT_NE(nullptr, e.error());
-}
-
-TEST_F(WireEncoderTest, GetValueSize3) {
- WireEncoder e(0x0300u);
- EXPECT_EQ(0u, e.GetValueSize(*v_empty)); // empty
- EXPECT_EQ(1u, e.GetValueSize(*v_boolean));
- EXPECT_EQ(8u, e.GetValueSize(*v_double));
- EXPECT_EQ(6u, e.GetValueSize(*v_string));
- EXPECT_EQ(6u, e.GetValueSize(*v_raw));
-
- EXPECT_EQ(1u + 3u, e.GetValueSize(*v_boolean_array));
- // truncated
- EXPECT_EQ(1u + 255u, e.GetValueSize(*v_boolean_array_big));
-
- EXPECT_EQ(1u + 2u * 8u, e.GetValueSize(*v_double_array));
- // truncated
- EXPECT_EQ(1u + 255u * 8u, e.GetValueSize(*v_double_array_big));
-
- EXPECT_EQ(1u + 6u + 8u, e.GetValueSize(*v_string_array));
- // truncated
- EXPECT_EQ(1u + 255u * 2u, e.GetValueSize(*v_string_array_big));
-}
-
-TEST_F(WireEncoderTest, WriteBooleanValue3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_boolean);
- auto v_false = Value::MakeBoolean(false);
- e.WriteValue(*v_false);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(2u, e.size());
- ASSERT_EQ("\x01\x00"sv, std::string_view(e.data(), e.size()));
-}
-
-TEST_F(WireEncoderTest, WriteDoubleValue3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_double);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(8u, e.size());
- ASSERT_EQ("\x3f\xf0\x00\x00\x00\x00\x00\x00"sv,
- std::string_view(e.data(), e.size()));
-}
-
-TEST_F(WireEncoderTest, WriteStringValue3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_string);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(6u, e.size());
- ASSERT_EQ("\x05hello"sv, std::string_view(e.data(), e.size()));
-}
-
-TEST_F(WireEncoderTest, WriteRawValue3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_raw);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(6u, e.size());
- ASSERT_EQ("\x05hello"sv, std::string_view(e.data(), e.size()));
-}
-
-TEST_F(WireEncoderTest, WriteBooleanArrayValue3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_boolean_array);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 3u, e.size());
- ASSERT_EQ("\x03\x00\x01\x00"sv, std::string_view(e.data(), e.size()));
-
- // truncated
- e.Reset();
- e.WriteValue(*v_boolean_array_big);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 255u, e.size());
- ASSERT_EQ("\xff\x00"sv, std::string_view(e.data(), 2));
-}
-
-TEST_F(WireEncoderTest, WriteDoubleArrayValue3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_double_array);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 2u * 8u, e.size());
- ASSERT_EQ(std::string_view("\x02\x3f\xe0\x00\x00\x00\x00\x00\x00"
- "\x3f\xd0\x00\x00\x00\x00\x00\x00",
- 17),
- std::string_view(e.data(), e.size()));
-
- // truncated
- e.Reset();
- e.WriteValue(*v_double_array_big);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 255u * 8u, e.size());
- ASSERT_EQ("\xff\x00"sv, std::string_view(e.data(), 2));
-}
-
-TEST_F(WireEncoderTest, WriteStringArrayValue3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_string_array);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 6u + 8u, e.size());
- ASSERT_EQ("\x02\x05hello\x07goodbye"sv, std::string_view(e.data(), e.size()));
-
- // truncated
- e.Reset();
- e.WriteValue(*v_string_array_big);
- ASSERT_EQ(nullptr, e.error());
- ASSERT_EQ(1u + 255u * 2u, e.size());
- ASSERT_EQ("\xff\x01"sv, std::string_view(e.data(), 2));
-}
-
-TEST_F(WireEncoderTest, WriteValueError3) {
- WireEncoder e(0x0300u);
- e.WriteValue(*v_empty); // empty
- ASSERT_EQ(0u, e.size());
- ASSERT_NE(nullptr, e.error());
-}
-
-TEST_F(WireEncoderTest, GetStringSize2) {
- // 2-byte length
- WireEncoder e(0x0200u);
- EXPECT_EQ(7u, e.GetStringSize(s_normal));
- EXPECT_EQ(130u, e.GetStringSize(s_long));
- // truncated
- EXPECT_EQ(65537u, e.GetStringSize(s_big));
-}
-
-TEST_F(WireEncoderTest, WriteString2) {
- WireEncoder e(0x0200u);
- e.WriteString(s_normal);
- EXPECT_EQ(nullptr, e.error());
- EXPECT_EQ(7u, e.size());
- EXPECT_EQ("\x00\x05hello"sv, std::string_view(e.data(), e.size()));
-
- e.Reset();
- e.WriteString(s_long);
- EXPECT_EQ(nullptr, e.error());
- ASSERT_EQ(130u, e.size());
- EXPECT_EQ("\x00\x80**"sv, std::string_view(e.data(), 4));
- EXPECT_EQ('*', e.data()[128]);
- EXPECT_EQ('x', e.data()[129]);
-
- // truncated
- e.Reset();
- e.WriteString(s_big);
- EXPECT_EQ(nullptr, e.error());
- ASSERT_EQ(65537u, e.size());
- EXPECT_EQ("\xff\xff**"sv, std::string_view(e.data(), 4));
- EXPECT_EQ('*', e.data()[65535]);
- EXPECT_EQ('x', e.data()[65536]);
-}
-
-TEST_F(WireEncoderTest, GetStringSize3) {
- // leb128-encoded length
- WireEncoder e(0x0300u);
- EXPECT_EQ(6u, e.GetStringSize(s_normal));
- EXPECT_EQ(130u, e.GetStringSize(s_long));
- EXPECT_EQ(65540u, e.GetStringSize(s_big));
-}
-
-TEST_F(WireEncoderTest, WriteString3) {
- WireEncoder e(0x0300u);
- e.WriteString(s_normal);
- EXPECT_EQ(nullptr, e.error());
- EXPECT_EQ(6u, e.size());
- EXPECT_EQ("\x05hello"sv, std::string_view(e.data(), e.size()));
-
- e.Reset();
- e.WriteString(s_long);
- EXPECT_EQ(nullptr, e.error());
- ASSERT_EQ(130u, e.size());
- EXPECT_EQ("\x80\x01**"sv, std::string_view(e.data(), 4));
- EXPECT_EQ('*', e.data()[128]);
- EXPECT_EQ('x', e.data()[129]);
-
- // NOT truncated
- e.Reset();
- e.WriteString(s_big);
- EXPECT_EQ(nullptr, e.error());
- ASSERT_EQ(65540u, e.size());
- EXPECT_EQ("\x81\x80\x04*"sv, std::string_view(e.data(), 4));
- EXPECT_EQ('*', e.data()[65536]);
- EXPECT_EQ('x', e.data()[65537]);
- EXPECT_EQ('x', e.data()[65538]);
- EXPECT_EQ('x', e.data()[65539]);
-}
-
-} // namespace nt
diff --git a/ntcore/src/test/native/cpp/main.cpp b/ntcore/src/test/native/cpp/main.cpp
index a3fec50..6d37027 100644
--- a/ntcore/src/test/native/cpp/main.cpp
+++ b/ntcore/src/test/native/cpp/main.cpp
@@ -8,14 +8,25 @@
#include "ntcore.h"
int main(int argc, char** argv) {
- nt::AddLogger(
- nt::GetDefaultInstance(),
- [](const nt::LogMessage& msg) {
- std::fputs(msg.message.c_str(), stderr);
- std::fputc('\n', stderr);
- },
- 0, UINT_MAX);
+ nt::AddLogger(nt::GetDefaultInstance(), 0, UINT_MAX, [](auto& event) {
+ if (auto msg = event.GetLogMessage()) {
+ std::fputs(msg->message.c_str(), stderr);
+ std::fputc('\n', stderr);
+ }
+ });
::testing::InitGoogleMock(&argc, argv);
int ret = RUN_ALL_TESTS();
return ret;
}
+
+extern "C" {
+void __ubsan_on_report(void) {
+ FAIL() << "Encountered an undefined behavior sanitizer error";
+}
+void __asan_on_error(void) {
+ FAIL() << "Encountered an address sanitizer error";
+}
+void __tsan_on_report(void) {
+ FAIL() << "Encountered a thread sanitizer error";
+}
+} // extern "C"
diff --git a/ntcore/src/test/native/cpp/net/MockNetworkInterface.h b/ntcore/src/test/native/cpp/net/MockNetworkInterface.h
new file mode 100644
index 0000000..b033436
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net/MockNetworkInterface.h
@@ -0,0 +1,69 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <string>
+
+#include <wpi/json.h>
+
+#include "PubSubOptions.h"
+#include "gmock/gmock.h"
+#include "net/NetworkInterface.h"
+
+namespace nt::net {
+
+class MockLocalInterface : public LocalInterface {
+ public:
+ MOCK_METHOD(NT_Topic, NetworkAnnounce,
+ (std::string_view name, std::string_view typeStr,
+ const wpi::json& properties, NT_Publisher pubHandle),
+ (override));
+ MOCK_METHOD(void, NetworkUnannounce, (std::string_view name), (override));
+ MOCK_METHOD(void, NetworkPropertiesUpdate,
+ (std::string_view name, const wpi::json& update, bool ack),
+ (override));
+ MOCK_METHOD(void, NetworkSetValue, (NT_Topic topicHandle, const Value& value),
+ (override));
+};
+
+class MockNetworkInterface : public NetworkInterface {
+ public:
+ MOCK_METHOD(void, Publish,
+ (NT_Publisher pubHandle, NT_Topic topicHandle,
+ std::string_view name, std::string_view typeStr,
+ const wpi::json& properties, const PubSubOptionsImpl& options),
+ (override));
+ MOCK_METHOD(void, Unpublish, (NT_Publisher pubHandle, NT_Topic topicHandle),
+ (override));
+ MOCK_METHOD(void, SetProperties,
+ (NT_Topic topicHandle, std::string_view name,
+ const wpi::json& update),
+ (override));
+ MOCK_METHOD(void, Subscribe,
+ (NT_Subscriber subHandle, std::span<const std::string> prefixes,
+ const PubSubOptionsImpl& options),
+ (override));
+ MOCK_METHOD(void, Unsubscribe, (NT_Subscriber subHandle), (override));
+ MOCK_METHOD(void, SetValue, (NT_Publisher pubHandle, const Value& value),
+ (override));
+};
+
+class MockLocalStorage : public ILocalStorage {
+ public:
+ MOCK_METHOD(NT_Topic, NetworkAnnounce,
+ (std::string_view name, std::string_view typeStr,
+ const wpi::json& properties, NT_Publisher pubHandle),
+ (override));
+ MOCK_METHOD(void, NetworkUnannounce, (std::string_view name), (override));
+ MOCK_METHOD(void, NetworkPropertiesUpdate,
+ (std::string_view name, const wpi::json& update, bool ack),
+ (override));
+ MOCK_METHOD(void, NetworkSetValue, (NT_Topic topicHandle, const Value& value),
+ (override));
+ MOCK_METHOD(void, StartNetwork, (NetworkInterface * network), (override));
+ MOCK_METHOD(void, ClearNetwork, (), (override));
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/test/native/cpp/net/MockWireConnection.cpp b/ntcore/src/test/native/cpp/net/MockWireConnection.cpp
new file mode 100644
index 0000000..a5ddb1f
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net/MockWireConnection.cpp
@@ -0,0 +1,26 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "MockWireConnection.h"
+
+using namespace nt::net;
+
+void MockWireConnection::StartSendText() {
+ if (m_in_text) {
+ m_text_os << ',';
+ } else {
+ m_text_os << '[';
+ m_in_text = true;
+ }
+}
+
+void MockWireConnection::FinishSendText() {
+ if (m_in_text) {
+ m_text_os << ']';
+ m_in_text = false;
+ }
+ m_text_os.flush();
+ Text(m_text);
+ m_text.clear();
+}
diff --git a/ntcore/src/test/native/cpp/net/MockWireConnection.h b/ntcore/src/test/native/cpp/net/MockWireConnection.h
new file mode 100644
index 0000000..3909cab
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net/MockWireConnection.h
@@ -0,0 +1,54 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include <stdint.h>
+
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <wpi/raw_ostream.h>
+
+#include "gmock/gmock.h"
+#include "net/WireConnection.h"
+
+namespace nt::net {
+
+class MockWireConnection : public WireConnection {
+ public:
+ MockWireConnection() : m_text_os{m_text}, m_binary_os{m_binary} {}
+
+ MOCK_METHOD(bool, Ready, (), (const, override));
+
+ TextWriter SendText() override { return {m_text_os, *this}; }
+ BinaryWriter SendBinary() override { return {m_binary_os, *this}; }
+
+ MOCK_METHOD(void, Text, (std::string_view contents));
+ MOCK_METHOD(void, Binary, (std::span<const uint8_t> contents));
+
+ MOCK_METHOD(void, Flush, (), (override));
+
+ MOCK_METHOD(void, Disconnect, (std::string_view reason), (override));
+
+ protected:
+ void StartSendText() override;
+ void FinishSendText() override;
+ void StartSendBinary() override {}
+ void FinishSendBinary() override {
+ Binary(m_binary);
+ m_binary.resize(0);
+ }
+
+ private:
+ std::string m_text;
+ wpi::raw_string_ostream m_text_os;
+ std::vector<uint8_t> m_binary;
+ wpi::raw_uvector_ostream m_binary_os;
+ bool m_in_text{false};
+};
+
+} // namespace nt::net
diff --git a/ntcore/src/test/native/cpp/net/WireDecoderTest.cpp b/ntcore/src/test/native/cpp/net/WireDecoderTest.cpp
new file mode 100644
index 0000000..f893302
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net/WireDecoderTest.cpp
@@ -0,0 +1,214 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <wpi/SmallString.h>
+#include <wpi/raw_ostream.h>
+
+#include "../MockLogger.h"
+#include "../TestPrinters.h"
+#include "Handle.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "net/Message.h"
+#include "net/WireDecoder.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace std::string_view_literals;
+using testing::_;
+using testing::MockFunction;
+using testing::StrictMock;
+
+namespace nt {
+
+class MockClientMessageHandler : public net::ClientMessageHandler {
+ public:
+ MOCK_METHOD4(ClientPublish,
+ void(int64_t pubuid, std::string_view name,
+ std::string_view typeStr, const wpi::json& properties));
+ MOCK_METHOD1(ClientUnpublish, void(int64_t pubuid));
+ MOCK_METHOD2(ClientSetProperties,
+ void(std::string_view name, const wpi::json& update));
+ MOCK_METHOD3(ClientSubscribe,
+ void(int64_t subuid, std::span<const std::string> prefixes,
+ const PubSubOptionsImpl& options));
+ MOCK_METHOD1(ClientUnsubscribe, void(int64_t subuid));
+};
+
+class MockServerMessageHandler : public net::ServerMessageHandler {
+ public:
+ MOCK_METHOD5(ServerAnnounce,
+ void(std::string_view name, int64_t id, std::string_view typeStr,
+ const wpi::json& properties,
+ std::optional<int64_t> pubuid));
+ MOCK_METHOD2(ServerUnannounce, void(std::string_view name, int64_t id));
+ MOCK_METHOD3(ServerPropertiesUpdate,
+ void(std::string_view name, const wpi::json& update, bool ack));
+};
+
+class WireDecodeTextClientTest : public ::testing::Test {
+ public:
+ StrictMock<MockClientMessageHandler> handler;
+ StrictMock<wpi::MockLogger> logger;
+};
+
+class WireDecodeTextServerTest : public ::testing::Test {
+ public:
+ StrictMock<MockServerMessageHandler> handler;
+ StrictMock<wpi::MockLogger> logger;
+};
+
+TEST_F(WireDecodeTextClientTest, EmptyArray) {
+ net::WireDecodeText("[]", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorEmpty) {
+ EXPECT_CALL(
+ 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));
+ net::WireDecodeText("", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorBadJson1) {
+ EXPECT_CALL(
+ 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));
+ net::WireDecodeText("[", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorBadJson2) {
+ EXPECT_CALL(
+ 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));
+ net::WireDecodeText("[{", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorNotArray) {
+ EXPECT_CALL(logger, Call(_, _, _, "expected JSON array at top level"sv));
+ net::WireDecodeText("{}", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorMessageNotObject) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: expected message to be an object"sv));
+ net::WireDecodeText("[5]", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorNoMethodKey) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: no method key"sv));
+ net::WireDecodeText("[{}]", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorMethodNotString) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: method must be a string"sv));
+ net::WireDecodeText("[{\"method\":5}]", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorNoParamsKey) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: no params key"sv));
+ net::WireDecodeText("[{\"method\":\"a\"}]", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorParamsNotObject) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: params must be an object"sv));
+ net::WireDecodeText("[{\"method\":\"a\",\"params\":5}]", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, ErrorUnknownMethod) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: unrecognized method 'a'"sv));
+ net::WireDecodeText("[{\"method\":\"a\",\"params\":{}}]", handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, PublishPropsEmpty) {
+ EXPECT_CALL(handler,
+ ClientPublish(5, std::string_view{"test"},
+ std::string_view{"double"}, wpi::json::object()));
+ net::WireDecodeText(
+ "[{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"properties\":{},\"pubuid\":5,\"type\":\"double\"}}]",
+ handler, logger);
+
+ EXPECT_CALL(handler,
+ ClientPublish(5, std::string_view{"test"},
+ std::string_view{"double"}, wpi::json::object()));
+ net::WireDecodeText(
+ "[{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"pubuid\":5,\"type\":\"double\"}}]",
+ handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, PublishProps) {
+ wpi::json props = {{"k", 6}};
+ EXPECT_CALL(handler, ClientPublish(5, std::string_view{"test"},
+ std::string_view{"double"}, props));
+ net::WireDecodeText(
+ "[{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"properties\":{\"k\":6},"
+ "\"pubuid\":5,\"type\":\"double\"}}]",
+ handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, PublishPropsError) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: properties must be an object"sv));
+ net::WireDecodeText(
+ "[{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"properties\":[\"k\"],"
+ "\"pubuid\":5,\"type\":\"double\"}}]",
+ handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, PublishError) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: no name key"sv));
+ net::WireDecodeText(
+ "[{\"method\":\"publish\",\"params\":{"
+ "\"pubuid\":5,\"type\":\"double\"}}]",
+ handler, logger);
+
+ EXPECT_CALL(logger, Call(_, _, _, "0: no type key"sv));
+ net::WireDecodeText(
+ "[{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"pubuid\":5}}]",
+ handler, logger);
+
+ EXPECT_CALL(logger, Call(_, _, _, "0: no pubuid key"sv));
+ net::WireDecodeText(
+ "[{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"type\":\"double\"}}]",
+ handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, Unpublish) {
+ EXPECT_CALL(handler, ClientUnpublish(5));
+ net::WireDecodeText("[{\"method\":\"unpublish\",\"params\":{\"pubuid\":5}}]",
+ handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, UnpublishMultiple) {
+ EXPECT_CALL(handler, ClientUnpublish(5));
+ EXPECT_CALL(handler, ClientUnpublish(6));
+ net::WireDecodeText(
+ "[{\"method\":\"unpublish\",\"params\":{\"pubuid\":5}},{\"method\":"
+ "\"unpublish\",\"params\":{\"pubuid\":6}}]",
+ handler, logger);
+}
+
+TEST_F(WireDecodeTextClientTest, UnpublishError) {
+ EXPECT_CALL(logger, Call(_, _, _, "0: no pubuid key"sv));
+ net::WireDecodeText("[{\"method\":\"unpublish\",\"params\":{}}]", handler,
+ logger);
+
+ EXPECT_CALL(logger, Call(_, _, _, "0: pubuid must be a number"sv));
+ net::WireDecodeText(
+ "[{\"method\":\"unpublish\",\"params\":{\"pubuid\":\"5\"}}]", handler,
+ logger);
+}
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/net/WireEncoderTest.cpp b/ntcore/src/test/native/cpp/net/WireEncoderTest.cpp
new file mode 100644
index 0000000..9cc3187
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net/WireEncoderTest.cpp
@@ -0,0 +1,293 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <wpi/json.h>
+#include <wpi/raw_ostream.h>
+
+#include "../SpanMatcher.h"
+#include "../TestPrinters.h"
+#include "Handle.h"
+#include "PubSubOptions.h"
+#include "gmock/gmock-matchers.h"
+#include "gtest/gtest.h"
+#include "net/Message.h"
+#include "net/WireEncoder.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace std::string_view_literals;
+
+namespace nt {
+
+class WireEncoderTextTest : public ::testing::Test {
+ protected:
+ std::string out;
+ wpi::raw_string_ostream os{out};
+ wpi::json GetJson() { return wpi::json::parse(os.str()); }
+};
+
+class WireEncoderBinaryTest : public ::testing::Test {
+ protected:
+ std::vector<uint8_t> out;
+ wpi::raw_uvector_ostream os{out};
+};
+
+TEST_F(WireEncoderTextTest, PublishPropsEmpty) {
+ net::WireEncodePublish(os, 5, "test", "double", wpi::json::object());
+ ASSERT_EQ(
+ os.str(),
+ "{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"properties\":{},\"pubuid\":5,\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, PublishProps) {
+ net::WireEncodePublish(os, 5, "test", "double", {{"k", 6}});
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"properties\":{\"k\":6},"
+ "\"pubuid\":5,\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, Unpublish) {
+ net::WireEncodeUnpublish(os, 5);
+ ASSERT_EQ(os.str(), "{\"method\":\"unpublish\",\"params\":{\"pubuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, SetProperties) {
+ net::WireEncodeSetProperties(os, "test", {{"k", 6}});
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"setproperties\",\"params\":{"
+ "\"name\":\"test\",\"update\":{\"k\":6}}}");
+}
+
+TEST_F(WireEncoderTextTest, Subscribe) {
+ net::WireEncodeSubscribe(os, 5, std::span<const std::string_view>{{"a", "b"}},
+ PubSubOptions{});
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"subscribe\",\"params\":{"
+ "\"options\":{},\"topics\":[\"a\",\"b\"],\"subuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, SubscribeSendAll) {
+ PubSubOptionsImpl options;
+ options.sendAll = true;
+ net::WireEncodeSubscribe(os, 5, std::span<const std::string_view>{{"a", "b"}},
+ options);
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"subscribe\",\"params\":{"
+ "\"options\":{\"all\":true},\"topics\":[\"a\",\"b\"],"
+ "\"subuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, SubscribePeriodic) {
+ PubSubOptionsImpl options;
+ options.periodicMs = 500u;
+ net::WireEncodeSubscribe(os, 5, std::span<const std::string_view>{{"a", "b"}},
+ options);
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"subscribe\",\"params\":{"
+ "\"options\":{\"periodic\":0.5},\"topics\":[\"a\",\"b\"],"
+ "\"subuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, SubscribeAllOptions) {
+ PubSubOptionsImpl options;
+ options.sendAll = true;
+ options.periodicMs = 500u;
+ net::WireEncodeSubscribe(os, 5, std::span<const std::string_view>{{"a", "b"}},
+ options);
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"subscribe\",\"params\":{"
+ "\"options\":{\"all\":true,\"periodic\":0.5},"
+ "\"topics\":[\"a\",\"b\"],\"subuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, Unsubscribe) {
+ net::WireEncodeUnsubscribe(os, 5);
+ ASSERT_EQ(os.str(), "{\"method\":\"unsubscribe\",\"params\":{\"subuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, Announce) {
+ net::WireEncodeAnnounce(os, "test", 5, "double", wpi::json::object(),
+ std::nullopt);
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"announce\",\"params\":{\"id\":5,\"name\":\"test\","
+ "\"properties\":{},\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, AnnounceProperties) {
+ net::WireEncodeAnnounce(os, "test", 5, "double", {{"k", 6}}, std::nullopt);
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"announce\",\"params\":{\"id\":5,\"name\":\"test\","
+ "\"properties\":{\"k\":6},\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, AnnouncePubuid) {
+ net::WireEncodeAnnounce(os, "test", 5, "double", wpi::json::object(), 6);
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"announce\",\"params\":{\"id\":5,\"name\":\"test\","
+ "\"properties\":{},\"pubuid\":6,\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, Unannounce) {
+ net::WireEncodeUnannounce(os, "test", 5);
+ ASSERT_EQ(
+ os.str(),
+ "{\"method\":\"unannounce\",\"params\":{\"id\":5,\"name\":\"test\"}}");
+}
+
+TEST_F(WireEncoderTextTest, MessagePublish) {
+ net::ClientMessage msg{net::PublishMsg{
+ Handle{0, 5, Handle::kPublisher}, 0, "test", "double", {{"k", 6}}, {}}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"publish\",\"params\":{"
+ "\"name\":\"test\",\"properties\":{\"k\":6},"
+ "\"pubuid\":5,\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageUnpublish) {
+ net::ClientMessage msg{
+ net::UnpublishMsg{Handle{0, 5, Handle::kPublisher}, 0}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(), "{\"method\":\"unpublish\",\"params\":{\"pubuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageSetProperties) {
+ net::ClientMessage msg{net::SetPropertiesMsg{0, "test", {{"k", 6}}}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"setproperties\",\"params\":{"
+ "\"name\":\"test\",\"update\":{\"k\":6}}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageSubscribe) {
+ net::ClientMessage msg{
+ net::SubscribeMsg{Handle{0, 5, Handle::kSubscriber}, {"a", "b"}, {}}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"subscribe\",\"params\":{"
+ "\"options\":{},\"topics\":[\"a\",\"b\"],\"subuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageUnsubscribe) {
+ net::ClientMessage msg{
+ net::UnsubscribeMsg{Handle{0, 5, Handle::kSubscriber}}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(), "{\"method\":\"unsubscribe\",\"params\":{\"subuid\":5}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageAnnounce) {
+ net::ServerMessage msg{
+ net::AnnounceMsg{"test", 5, "double", std::nullopt, wpi::json::object()}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"announce\",\"params\":{\"id\":5,\"name\":\"test\","
+ "\"properties\":{},\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageAnnounceProperties) {
+ net::ServerMessage msg{
+ net::AnnounceMsg{"test", 5, "double", std::nullopt, {{"k", 6}}}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"announce\",\"params\":{\"id\":5,\"name\":\"test\","
+ "\"properties\":{\"k\":6},\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageAnnouncePubuid) {
+ net::ServerMessage msg{
+ net::AnnounceMsg{"test", 5, "double", 6, wpi::json::object()}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(os.str(),
+ "{\"method\":\"announce\",\"params\":{\"id\":5,\"name\":\"test\","
+ "\"properties\":{},\"pubuid\":6,\"type\":\"double\"}}");
+}
+
+TEST_F(WireEncoderTextTest, MessageUnannounce) {
+ net::ServerMessage msg{net::UnannounceMsg{"test", 5}};
+ ASSERT_TRUE(net::WireEncodeText(os, msg));
+ ASSERT_EQ(
+ os.str(),
+ "{\"method\":\"unannounce\",\"params\":{\"id\":5,\"name\":\"test\"}}");
+}
+
+TEST_F(WireEncoderTextTest, ServerMessageEmpty) {
+ ASSERT_FALSE(net::WireEncodeText(os, net::ServerMessage{}));
+}
+
+TEST_F(WireEncoderTextTest, ServerMessageValue) {
+ net::ServerMessage msg{net::ServerValueMsg{}};
+ ASSERT_FALSE(net::WireEncodeText(os, msg));
+}
+
+TEST_F(WireEncoderBinaryTest, Boolean) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeBoolean(true));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x00\xc3"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, Integer) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeInteger(7));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x02\x07"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, Float) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeFloat(2.5));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x03\xca\x40\x20\x00\x00"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, Double) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeDouble(2.5));
+ ASSERT_THAT(
+ out,
+ wpi::SpanEq("\x94\x05\x06\x01\xcb\x40\x04\x00\x00\x00\x00\x00\x00"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, String) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeString("hello"));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x04\xa5hello"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, Raw) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeRaw("hello"_us));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x05\xc4\x05hello"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, BooleanArray) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeBooleanArray({true, false, true}));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x10\x93\xc3\xc2\xc3"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, IntegerArray) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeIntegerArray({1, 2, 4}));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x12\x93\x01\x02\x04"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, FloatArray) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeFloatArray({1, 2, 3}));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x13\x93"
+ "\xca\x3f\x80\x00\x00"
+ "\xca\x40\x00\x00\x00"
+ "\xca\x40\x40\x00\x00"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, DoubleArray) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeDoubleArray({1, 2, 3}));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x11\x93"
+ "\xcb\x3f\xf0\x00\x00\x00\x00\x00\x00"
+ "\xcb\x40\x00\x00\x00\x00\x00\x00\x00"
+ "\xcb\x40\x08\x00\x00\x00\x00\x00\x00"_us));
+}
+
+TEST_F(WireEncoderBinaryTest, StringArray) {
+ net::WireEncodeBinary(os, 5, 6, Value::MakeStringArray({"hello", "bye"}));
+ ASSERT_THAT(out, wpi::SpanEq("\x94\x05\x06\x14\x92\xa5hello\xa3"
+ "bye"_us));
+}
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/net3/MessageMatcher3.cpp b/ntcore/src/test/native/cpp/net3/MessageMatcher3.cpp
new file mode 100644
index 0000000..d595c6a
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net3/MessageMatcher3.cpp
@@ -0,0 +1,45 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "MessageMatcher3.h"
+
+namespace nt::net3 {
+
+bool MessageMatcher::MatchAndExplain(
+ Message3 msg, ::testing::MatchResultListener* listener) const {
+ bool match = true;
+ if (msg.str() != goodmsg.str()) {
+ *listener << "str mismatch ";
+ match = false;
+ }
+ if ((!msg.value() && goodmsg.value()) || (msg.value() && !goodmsg.value()) ||
+ (msg.value() && goodmsg.value() && msg.value() != goodmsg.value())) {
+ *listener << "value mismatch ";
+ match = false;
+ }
+ if (msg.id() != goodmsg.id()) {
+ *listener << "id mismatch ";
+ match = false;
+ }
+ if (msg.flags() != goodmsg.flags()) {
+ *listener << "flags mismatch";
+ match = false;
+ }
+ if (msg.seq_num_uid() != goodmsg.seq_num_uid()) {
+ *listener << "seq_num_uid mismatch";
+ match = false;
+ }
+ return match;
+}
+
+void MessageMatcher::DescribeTo(::std::ostream* os) const {
+ PrintTo(goodmsg, os);
+}
+
+void MessageMatcher::DescribeNegationTo(::std::ostream* os) const {
+ *os << "is not equal to ";
+ PrintTo(goodmsg, os);
+}
+
+} // namespace nt::net3
diff --git a/ntcore/src/test/native/cpp/net3/MessageMatcher3.h b/ntcore/src/test/native/cpp/net3/MessageMatcher3.h
new file mode 100644
index 0000000..6b1e770
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net3/MessageMatcher3.h
@@ -0,0 +1,34 @@
+// 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 <memory>
+#include <ostream>
+#include <utility>
+
+#include "../TestPrinters.h"
+#include "gmock/gmock.h"
+#include "net3/Message3.h"
+
+namespace nt::net3 {
+
+class MessageMatcher : public ::testing::MatcherInterface<Message3> {
+ public:
+ explicit MessageMatcher(Message3 goodmsg_) : goodmsg(std::move(goodmsg_)) {}
+
+ bool MatchAndExplain(Message3 msg,
+ ::testing::MatchResultListener* listener) const override;
+ void DescribeTo(::std::ostream* os) const override;
+ void DescribeNegationTo(::std::ostream* os) const override;
+
+ private:
+ Message3 goodmsg;
+};
+
+inline ::testing::Matcher<Message3> MessageEq(Message3 goodmsg) {
+ return ::testing::MakeMatcher(new MessageMatcher(std::move(goodmsg)));
+}
+
+} // namespace nt::net3
diff --git a/ntcore/src/test/native/cpp/net3/MockWireConnection3.h b/ntcore/src/test/native/cpp/net3/MockWireConnection3.h
new file mode 100644
index 0000000..b7c785f
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net3/MockWireConnection3.h
@@ -0,0 +1,44 @@
+// 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 <span>
+#include <vector>
+
+#include <wpi/raw_ostream.h>
+
+#include "gmock/gmock.h"
+#include "net3/WireConnection3.h"
+
+namespace nt::net3 {
+
+class MockWireConnection3 : public WireConnection3 {
+ public:
+ MockWireConnection3() : m_os{m_data} {}
+
+ MOCK_METHOD(bool, Ready, (), (const, override));
+
+ Writer Send() override { return {m_os, *this}; }
+
+ MOCK_METHOD(void, Data, (std::span<const uint8_t> data));
+
+ MOCK_METHOD(void, Flush, (), (override));
+
+ MOCK_METHOD(void, Disconnect, (std::string_view reason), (override));
+
+ protected:
+ void FinishSend() override {
+ Data(m_data);
+ m_data.resize(0);
+ }
+
+ private:
+ std::vector<uint8_t> m_data;
+ wpi::raw_uvector_ostream m_os;
+};
+
+} // namespace nt::net3
diff --git a/ntcore/src/test/native/cpp/net3/WireDecoder3Test.cpp b/ntcore/src/test/native/cpp/net3/WireDecoder3Test.cpp
new file mode 100644
index 0000000..1cd6ecb
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net3/WireDecoder3Test.cpp
@@ -0,0 +1,276 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <stdint.h>
+
+#include <cfloat>
+#include <climits>
+#include <string>
+#include <string_view>
+
+#include "../SpanMatcher.h"
+#include "../TestPrinters.h"
+#include "../ValueMatcher.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "net3/WireDecoder3.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace std::string_view_literals;
+using testing::_;
+using testing::MockFunction;
+using testing::StrictMock;
+
+namespace nt {
+
+class MockMessageHandler3 : public net3::MessageHandler3 {
+ public:
+ MOCK_METHOD0(KeepAlive, void());
+ MOCK_METHOD0(ServerHelloDone, void());
+ MOCK_METHOD0(ClientHelloDone, void());
+ MOCK_METHOD0(ClearEntries, void());
+ MOCK_METHOD1(ProtoUnsup, void(unsigned int proto_rev));
+ MOCK_METHOD2(ClientHello,
+ void(std::string_view self_id, unsigned int proto_rev));
+ MOCK_METHOD2(ServerHello, void(unsigned int flags, std::string_view self_id));
+ MOCK_METHOD5(EntryAssign, void(std::string_view name, unsigned int id,
+ unsigned int seq_num, const Value& value,
+ unsigned int flags));
+ MOCK_METHOD3(EntryUpdate,
+ void(unsigned int id, unsigned int seq_num, const Value& value));
+ MOCK_METHOD2(FlagsUpdate, void(unsigned int id, unsigned int flags));
+ MOCK_METHOD1(EntryDelete, void(unsigned int id));
+ MOCK_METHOD3(ExecuteRpc, void(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> params));
+ MOCK_METHOD3(RpcResponse, void(unsigned int id, unsigned int uid,
+ std::span<const uint8_t> result));
+};
+
+class WireDecoder3Test : public ::testing::Test {
+ protected:
+ StrictMock<MockMessageHandler3> handler;
+ net3::WireDecoder3 decoder{handler};
+
+ void DecodeComplete(std::span<const uint8_t> in) {
+ decoder.Execute(&in);
+ EXPECT_TRUE(in.empty());
+ ASSERT_EQ(decoder.GetError(), "");
+ }
+};
+
+TEST_F(WireDecoder3Test, KeepAlive) {
+ EXPECT_CALL(handler, KeepAlive());
+ DecodeComplete("\x00"_us);
+}
+
+TEST_F(WireDecoder3Test, ClientHello) {
+ EXPECT_CALL(handler, ClientHello(std::string_view{"hello"}, 0x0300u));
+ DecodeComplete("\x01\x03\x00\x05hello"_us);
+}
+
+TEST_F(WireDecoder3Test, ProtoUnsup) {
+ EXPECT_CALL(handler, ProtoUnsup(0x0300u));
+ EXPECT_CALL(handler, ProtoUnsup(0x0200u));
+ DecodeComplete("\x02\x03\x00\x02\x02\x00"_us);
+}
+
+TEST_F(WireDecoder3Test, ServerHelloDone) {
+ EXPECT_CALL(handler, ServerHelloDone());
+ DecodeComplete("\x03"_us);
+}
+
+TEST_F(WireDecoder3Test, ServerHello) {
+ EXPECT_CALL(handler, ServerHello(0x03, std::string_view{"hello"}));
+ DecodeComplete("\x04\x03\x05hello"_us);
+}
+
+TEST_F(WireDecoder3Test, ClientHelloDone) {
+ EXPECT_CALL(handler, ClientHelloDone());
+ DecodeComplete("\x05"_us);
+}
+
+TEST_F(WireDecoder3Test, FlagsUpdate) {
+ EXPECT_CALL(handler, FlagsUpdate(0x5678, 0x03));
+ DecodeComplete("\x12\x56\x78\x03"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryDelete) {
+ EXPECT_CALL(handler, EntryDelete(0x5678));
+ DecodeComplete("\x13\x56\x78"_us);
+}
+
+TEST_F(WireDecoder3Test, ClearEntries) {
+ EXPECT_CALL(handler, ClearEntries());
+ DecodeComplete("\x14\xd0\x6c\xb2\x7a"_us);
+}
+
+TEST_F(WireDecoder3Test, ClearEntriesInvalid) {
+ auto in = "\x14\xd0\x6c\xb2\x7b"_us;
+ decoder.Execute(&in);
+ EXPECT_EQ(decoder.GetError(), "received incorrect CLEAR_ENTRIES magic value");
+}
+
+TEST_F(WireDecoder3Test, ExecuteRpc) {
+ EXPECT_CALL(handler, ExecuteRpc(0x5678, 0x1234, wpi::SpanEq("hello"_us)));
+ DecodeComplete("\x20\x56\x78\x12\x34\x05hello"_us);
+}
+
+TEST_F(WireDecoder3Test, RpcResponse) {
+ EXPECT_CALL(handler, RpcResponse(0x5678, 0x1234, wpi::SpanEq("hello"_us)));
+ DecodeComplete("\x21\x56\x78\x12\x34\x05hello"_us);
+}
+
+TEST_F(WireDecoder3Test, UnknownMessage) {
+ auto in = "\x23"_us;
+ decoder.Execute(&in);
+ EXPECT_EQ(decoder.GetError(), "unrecognized message type: 35");
+}
+
+TEST_F(WireDecoder3Test, EntryAssignBoolean) {
+ EXPECT_CALL(handler, EntryAssign("test"sv, 0x5678, 0x1234,
+ Value::MakeBoolean(true), 0x9a));
+ DecodeComplete("\x10\x04test\x00\x56\x78\x12\x34\x9a\x01"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryAssignDouble) {
+ EXPECT_CALL(handler, EntryAssign("test"sv, 0x5678, 0x1234,
+ Value::MakeDouble(2.3e5), 0x9a));
+ DecodeComplete(
+ "\x10\x04test\x01\x56\x78\x12\x34"
+ "\x9a\x41\x0c\x13\x80\x00\x00\x00\x00"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateBoolean) {
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234, Value::MakeBoolean(true)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x00\x01"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateDouble) {
+ // values except min and max from
+ // http://www.binaryconvert.com/result_double.html
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234, Value::MakeDouble(0.0)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00"_us);
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234, Value::MakeDouble(2.3e5)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x01\x41\x0c\x13\x80\x00\x00\x00\x00"_us);
+ EXPECT_CALL(
+ handler,
+ EntryUpdate(0x5678, 0x1234,
+ Value::MakeDouble(std::numeric_limits<double>::infinity())));
+ DecodeComplete("\x11\x56\x78\x12\x34\x01\x7f\xf0\x00\x00\x00\x00\x00\x00"_us);
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234, Value::MakeDouble(DBL_MIN)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x01\x00\x10\x00\x00\x00\x00\x00\x00"_us);
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234, Value::MakeDouble(DBL_MAX)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x01\x7f\xef\xff\xff\xff\xff\xff\xff"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateString) {
+ EXPECT_CALL(handler,
+ EntryUpdate(0x5678, 0x1234, Value::MakeString("hello"sv)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x02\x05hello"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateString2) {
+ std::vector<uint8_t> in{0x11, 0x56, 0x78, 0x12, 0x34, 0x02, 0x7f};
+ in.insert(in.end(), 127, '*');
+ std::string out(127, '*');
+ EXPECT_CALL(handler,
+ EntryUpdate(0x5678, 0x1234, Value::MakeString(std::move(out))));
+ DecodeComplete(in);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateStringLarge) {
+ std::vector<uint8_t> in{0x11, 0x56, 0x78, 0x12, 0x34, 0x02, 0x80, 0x01};
+ in.insert(in.end(), 127, '*');
+ in.push_back('x');
+
+ std::string out(127, '*');
+ out.push_back('x');
+
+ EXPECT_CALL(handler,
+ EntryUpdate(0x5678, 0x1234, Value::MakeString(std::move(out))));
+ DecodeComplete(in);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateStringHuge) {
+ std::vector<uint8_t> in{0x11, 0x56, 0x78, 0x12, 0x34, 0x02, 0x81, 0x80, 0x04};
+ in.insert(in.end(), 65534, '*');
+ in.insert(in.end(), 3, 'x');
+
+ std::string out(65534, '*');
+ out.append(3, 'x');
+
+ EXPECT_CALL(handler,
+ EntryUpdate(0x5678, 0x1234, Value::MakeString(std::move(out))));
+ DecodeComplete(in);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateRaw) {
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234, Value::MakeRaw("hello"_us)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x03\x05hello"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateBooleanArray) {
+ EXPECT_CALL(handler,
+ EntryUpdate(0x5678, 0x1234,
+ Value::MakeBooleanArray({false, true, false})));
+ DecodeComplete("\x11\x56\x78\x12\x34\x10\x03\x00\x01\x00"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateBooleanArrayLarge) {
+ std::vector<uint8_t> in{0x11, 0x56, 0x78, 0x12, 0x34, 0x10, 0xff};
+ in.insert(in.end(), 255, 0);
+ std::vector<int> out(255, 0);
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234,
+ Value::MakeBooleanArray(std::move(out))));
+ DecodeComplete(in);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateDoubleArray) {
+ EXPECT_CALL(handler,
+ EntryUpdate(0x5678, 0x1234, Value::MakeDoubleArray({0.5, 0.25})));
+ DecodeComplete(
+ "\x11\x56\x78\x12\x34\x11\x02"
+ "\x3f\xe0\x00\x00\x00\x00\x00\x00"
+ "\x3f\xd0\x00\x00\x00\x00\x00\x00"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateDoubleArrayLarge) {
+ std::vector<uint8_t> in{0x11, 0x56, 0x78, 0x12, 0x34, 0x11, 0xff};
+ in.insert(in.end(), 255 * 8, 0);
+ std::vector<double> out(255, 0.0);
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234,
+ Value::MakeDoubleArray(std::move(out))));
+ DecodeComplete(in);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateStringArray) {
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234,
+ Value::MakeStringArray({"hello", "bye"})));
+ DecodeComplete(
+ "\x11\x56\x78\x12\x34\x12\x02\x05hello\x03"
+ "bye"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateStringArrayLarge) {
+ std::vector<uint8_t> in{0x11, 0x56, 0x78, 0x12, 0x34, 0x12, 0xff};
+ in.insert(in.end(), 255, 0);
+ std::vector<std::string> out(255, "");
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234,
+ Value::MakeStringArray(std::move(out))));
+ DecodeComplete(in);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateRpc) {
+ // RPC values are decoded as raw
+ EXPECT_CALL(handler, EntryUpdate(0x5678, 0x1234, Value::MakeRaw("hello"_us)));
+ DecodeComplete("\x11\x56\x78\x12\x34\x20\x05hello"_us);
+}
+
+TEST_F(WireDecoder3Test, EntryUpdateTypeError) {
+ auto in = "\x11\x56\x78\x12\x34\x30\x11"_us;
+ decoder.Execute(&in);
+ ASSERT_EQ(decoder.GetError(), "unrecognized value type");
+}
+
+} // namespace nt
diff --git a/ntcore/src/test/native/cpp/net3/WireEncoder3Test.cpp b/ntcore/src/test/native/cpp/net3/WireEncoder3Test.cpp
new file mode 100644
index 0000000..bb4bfd2
--- /dev/null
+++ b/ntcore/src/test/native/cpp/net3/WireEncoder3Test.cpp
@@ -0,0 +1,283 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include <cfloat>
+#include <climits>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <wpi/raw_ostream.h>
+
+#include "../SpanMatcher.h"
+#include "../TestPrinters.h"
+#include "gtest/gtest.h"
+#include "net3/Message3.h"
+#include "net3/WireEncoder3.h"
+#include "networktables/NetworkTableValue.h"
+
+using namespace std::string_view_literals;
+
+namespace nt {
+
+class WireEncoder3Test : public ::testing::Test {
+ protected:
+ std::vector<uint8_t> out;
+ wpi::raw_uvector_ostream os{out};
+};
+
+TEST_F(WireEncoder3Test, Unknown) {
+ net3::WireEncode(os, net3::Message3{});
+ ASSERT_TRUE(out.empty());
+}
+
+TEST_F(WireEncoder3Test, KeepAlive) {
+ net3::WireEncode(os, net3::Message3::KeepAlive());
+ ASSERT_THAT(out, wpi::SpanEq("\x00"_us));
+}
+
+TEST_F(WireEncoder3Test, ClientHello) {
+ net3::WireEncode(os, net3::Message3::ClientHello("hello"));
+ ASSERT_THAT(out, wpi::SpanEq("\x01\x03\x00\x05hello"_us));
+}
+
+TEST_F(WireEncoder3Test, ProtoUnsup) {
+ net3::WireEncode(os, net3::Message3::ProtoUnsup());
+ net3::WireEncode(os, net3::Message3::ProtoUnsup(0x0200u));
+ ASSERT_THAT(out, wpi::SpanEq("\x02\x03\x00\x02\x02\x00"_us));
+}
+
+TEST_F(WireEncoder3Test, ServerHelloDone) {
+ net3::WireEncode(os, net3::Message3::ServerHelloDone());
+ ASSERT_THAT(out, wpi::SpanEq("\x03"_us));
+}
+
+TEST_F(WireEncoder3Test, ServerHello) {
+ net3::WireEncode(os, net3::Message3::ServerHello(0x03, "hello"));
+ ASSERT_THAT(out, wpi::SpanEq("\x04\x03\x05hello"_us));
+}
+
+TEST_F(WireEncoder3Test, ClientHelloDone) {
+ net3::WireEncode(os, net3::Message3::ClientHelloDone());
+ ASSERT_THAT(out, wpi::SpanEq("\x05"_us));
+}
+
+TEST_F(WireEncoder3Test, FlagsUpdate) {
+ net3::WireEncode(os, net3::Message3::FlagsUpdate(0x5678, 0x03));
+ ASSERT_THAT(out, wpi::SpanEq("\x12\x56\x78\x03"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryDelete) {
+ net3::WireEncode(os, net3::Message3::EntryDelete(0x5678));
+ ASSERT_THAT(out, wpi::SpanEq("\x13\x56\x78"_us));
+}
+
+TEST_F(WireEncoder3Test, ClearEntries) {
+ net3::WireEncode(os, net3::Message3::ClearEntries());
+ ASSERT_THAT(out, wpi::SpanEq("\x14\xd0\x6c\xb2\x7a"_us));
+}
+
+TEST_F(WireEncoder3Test, ExecuteRpc) {
+ net3::WireEncode(os, net3::Message3::ExecuteRpc(0x5678, 0x1234, "hello"_us));
+ ASSERT_THAT(out, wpi::SpanEq("\x20\x56\x78\x12\x34\x05hello"_us));
+}
+
+TEST_F(WireEncoder3Test, RpcResponse) {
+ net3::WireEncode(os, net3::Message3::RpcResponse(0x5678, 0x1234, "hello"_us));
+ ASSERT_THAT(out, wpi::SpanEq("\x21\x56\x78\x12\x34\x05hello"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryAssignBoolean) {
+ net3::WireEncode(os,
+ net3::Message3::EntryAssign("test"sv, 0x5678, 0x1234,
+ Value::MakeBoolean(true), 0x9a));
+ ASSERT_THAT(out, wpi::SpanEq("\x10\x04test\x00\x56\x78\x12\x34\x9a\x01"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryAssignDouble) {
+ net3::WireEncode(os,
+ net3::Message3::EntryAssign("test"sv, 0x5678, 0x1234,
+ Value::MakeDouble(2.3e5), 0x9a));
+ ASSERT_THAT(out, wpi::SpanEq("\x10\x04test\x01\x56\x78\x12\x34"
+ "\x9a\x41\x0c\x13\x80\x00\x00\x00\x00"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateBoolean) {
+ net3::WireEncode(os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeBoolean(true)));
+ ASSERT_THAT(out, wpi::SpanEq("\x11\x56\x78\x12\x34\x00\x01"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateDouble) {
+ // values except min and max from
+ // http://www.binaryconvert.com/result_double.html
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234, Value::MakeDouble(0.0)));
+ ASSERT_THAT(
+ out, wpi::SpanEq(
+ "\x11\x56\x78\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00"_us));
+
+ out.clear();
+ net3::WireEncode(os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeDouble(2.3e5)));
+ ASSERT_THAT(
+ out, wpi::SpanEq(
+ "\x11\x56\x78\x12\x34\x01\x41\x0c\x13\x80\x00\x00\x00\x00"_us));
+
+ out.clear();
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(
+ 0x5678, 0x1234,
+ Value::MakeDouble(std::numeric_limits<double>::infinity())));
+ ASSERT_THAT(
+ out, wpi::SpanEq(
+ "\x11\x56\x78\x12\x34\x01\x7f\xf0\x00\x00\x00\x00\x00\x00"_us));
+
+ out.clear();
+ net3::WireEncode(os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeDouble(DBL_MIN)));
+ ASSERT_THAT(
+ out, wpi::SpanEq(
+ "\x11\x56\x78\x12\x34\x01\x00\x10\x00\x00\x00\x00\x00\x00"_us));
+
+ out.clear();
+ net3::WireEncode(os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeDouble(DBL_MAX)));
+ ASSERT_THAT(
+ out, wpi::SpanEq(
+ "\x11\x56\x78\x12\x34\x01\x7f\xef\xff\xff\xff\xff\xff\xff"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateString) {
+ net3::WireEncode(os, net3::Message3::EntryUpdate(
+ 0x5678, 0x1234, Value::MakeString("hello"sv)));
+ ASSERT_THAT(out, wpi::SpanEq("\x11\x56\x78\x12\x34\x02\x05hello"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateString2) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x02, 0x7f};
+ ex.insert(ex.end(), 127, '*');
+ std::string in(127, '*');
+ net3::WireEncode(os, net3::Message3::EntryUpdate(
+ 0x5678, 0x1234, Value::MakeString(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateStringLarge) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x02, 0x80, 0x01};
+ ex.insert(ex.end(), 127, '*');
+ ex.push_back('x');
+
+ std::string in(127, '*');
+ in.push_back('x');
+
+ net3::WireEncode(os, net3::Message3::EntryUpdate(
+ 0x5678, 0x1234, Value::MakeString(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateStringHuge) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x02, 0x81, 0x80, 0x04};
+ ex.insert(ex.end(), 65534, '*');
+ ex.insert(ex.end(), 3, 'x');
+
+ std::string in(65534, '*');
+ in.append(3, 'x');
+
+ net3::WireEncode(os, net3::Message3::EntryUpdate(
+ 0x5678, 0x1234, Value::MakeString(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateRaw) {
+ net3::WireEncode(os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeRaw("hello"_us)));
+ ASSERT_THAT(out, wpi::SpanEq("\x11\x56\x78\x12\x34\x03\x05hello"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateBooleanArray) {
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(
+ 0x5678, 0x1234, Value::MakeBooleanArray({false, true, false})));
+ ASSERT_THAT(out, wpi::SpanEq("\x11\x56\x78\x12\x34\x10\x03\x00\x01\x00"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateBooleanArrayLarge) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x10, 0xff};
+ ex.insert(ex.end(), 255, 0);
+ std::vector<int> in(255, 0);
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeBooleanArray(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateBooleanArrayTrunc) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x10, 0xff};
+ ex.insert(ex.end(), 255, 0);
+ std::vector<int> in(256, 0);
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeBooleanArray(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateDoubleArray) {
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeDoubleArray({0.5, 0.25})));
+ ASSERT_THAT(out, wpi::SpanEq("\x11\x56\x78\x12\x34\x11\x02"
+ "\x3f\xe0\x00\x00\x00\x00\x00\x00"
+ "\x3f\xd0\x00\x00\x00\x00\x00\x00"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateDoubleArrayLarge) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x11, 0xff};
+ ex.insert(ex.end(), 255 * 8, 0);
+ std::vector<double> in(255, 0.0);
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeDoubleArray(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateDoubleArrayTrunc) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x11, 0xff};
+ ex.insert(ex.end(), 255 * 8, 0);
+ std::vector<double> in(256, 0.0);
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeDoubleArray(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateStringArray) {
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(
+ 0x5678, 0x1234, Value::MakeStringArray({"hello", "bye"})));
+ ASSERT_THAT(out, wpi::SpanEq("\x11\x56\x78\x12\x34\x12\x02\x05hello\x03"
+ "bye"_us));
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateStringArrayLarge) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x12, 0xff};
+ ex.insert(ex.end(), 255, 0);
+ std::vector<std::string> in(255, "");
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeStringArray(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+TEST_F(WireEncoder3Test, EntryUpdateStringArrayTrunc) {
+ std::vector<uint8_t> ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x12, 0xff};
+ ex.insert(ex.end(), 255, 0);
+ std::vector<std::string> in(256, "");
+ net3::WireEncode(
+ os, net3::Message3::EntryUpdate(0x5678, 0x1234,
+ Value::MakeStringArray(std::move(in))));
+ ASSERT_THAT(out, ex);
+}
+
+} // namespace nt