From 77301b126c4a5eb4620375d36b3125b0c1016ed9 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 8 Oct 2022 10:01:31 -0700 Subject: [PATCH] [ntcore] NetworkTables 4 (#3217) --- .github/workflows/sanitizers.yml | 4 +- cameraserver/build.gradle | 2 +- cameraserver/multiCameraServer/build.gradle | 2 +- .../src/main/java/edu/wpi/Main.java | 3 +- .../src/main/native/cpp/main.cpp | 4 +- .../wpi/first/cameraserver/CameraServer.java | 521 ++-- .../native/cpp/cameraserver/CameraServer.cpp | 346 ++- docs/build.gradle | 1 + glass/.styleguide | 1 + glass/build.gradle | 4 +- glass/src/app/native/cpp/main.cpp | 24 +- glass/src/lib/native/cpp/other/Plot.cpp | 6 +- .../lib/native/cpp/support/EnumSetting.cpp | 32 +- .../src/lib/native/include/glass/DataSource.h | 11 +- .../include/glass/other/StringChooser.h | 3 - .../include/glass/support/EnumSetting.h | 7 +- .../libnt/native/cpp/NTCommandScheduler.cpp | 53 +- .../libnt/native/cpp/NTCommandSelector.cpp | 36 +- .../libnt/native/cpp/NTDifferentialDrive.cpp | 66 +- glass/src/libnt/native/cpp/NTDigitalInput.cpp | 32 +- .../src/libnt/native/cpp/NTDigitalOutput.cpp | 45 +- glass/src/libnt/native/cpp/NTFMS.cpp | 84 +- glass/src/libnt/native/cpp/NTField2D.cpp | 187 +- glass/src/libnt/native/cpp/NTGyro.cpp | 35 +- glass/src/libnt/native/cpp/NTMecanumDrive.cpp | 105 +- glass/src/libnt/native/cpp/NTMechanism2D.cpp | 279 +- .../src/libnt/native/cpp/NTPIDController.cpp | 93 +- .../libnt/native/cpp/NTSpeedController.cpp | 48 +- .../src/libnt/native/cpp/NTStringChooser.cpp | 93 +- glass/src/libnt/native/cpp/NTSubsystem.cpp | 43 +- glass/src/libnt/native/cpp/NetworkTables.cpp | 1372 +++++++-- .../libnt/native/cpp/NetworkTablesHelper.cpp | 19 - .../native/cpp/NetworkTablesProvider.cpp | 94 +- .../native/cpp/NetworkTablesSettings.cpp | 50 +- .../native/cpp/StandardNetworkTables.cpp | 28 +- .../glass/networktables/NTCommandScheduler.h | 22 +- .../glass/networktables/NTCommandSelector.h | 13 +- .../glass/networktables/NTDifferentialDrive.h | 19 +- .../glass/networktables/NTDigitalInput.h | 13 +- .../glass/networktables/NTDigitalOutput.h | 15 +- .../include/glass/networktables/NTFMS.h | 18 +- .../include/glass/networktables/NTField2D.h | 15 +- .../include/glass/networktables/NTGyro.h | 13 +- .../glass/networktables/NTMecanumDrive.h | 22 +- .../glass/networktables/NTMechanism2D.h | 20 +- .../glass/networktables/NTPIDController.h | 22 +- .../glass/networktables/NTSpeedController.h | 16 +- .../glass/networktables/NTStringChooser.h | 20 +- .../include/glass/networktables/NTSubsystem.h | 14 +- .../glass/networktables/NetworkTables.h | 172 +- .../glass/networktables/NetworkTablesHelper.h | 55 - .../networktables/NetworkTablesProvider.h | 44 +- .../networktables/NetworkTablesSettings.h | 6 +- myRobot/build.gradle | 6 +- ntcore/CMakeLists.txt | 35 +- ntcore/build.gradle | 297 ++ ntcore/doc/networktables4.adoc | 819 ++++++ ntcore/generate_topics.py | 121 + ntcore/src/dev/native/cpp/main.cpp | 102 +- .../src/generate/cpp/jni/types_jni.cpp.jinja | 245 ++ .../src/generate/cpp/ntcore_c_types.cpp.jinja | 106 + .../generate/cpp/ntcore_cpp_types.cpp.jinja | 76 + .../include/networktables/Topic.h.jinja | 437 +++ .../include/networktables/Topic.inc.jinja | 135 + .../generate/include/ntcore_c_types.h.jinja | 151 + .../generate/include/ntcore_cpp_types.h.jinja | 155 + ntcore/src/generate/java/Entry.java.jinja | 15 + ntcore/src/generate/java/EntryImpl.java.jinja | 75 + .../generate/java/GenericEntryImpl.java.jinja | 317 +++ .../generate/java/GenericPublisher.java.jinja | 119 + .../java/GenericSubscriber.java.jinja | 58 + .../java/NetworkTableEntry.java.jinja | 537 ++++ .../java/NetworkTableInstance.java.jinja | 852 ++++++ .../java/NetworkTableValue.java.jinja | 248 ++ .../generate/java/NetworkTablesJNI.java.jinja | 301 ++ ntcore/src/generate/java/Publisher.java.jinja | 49 + .../src/generate/java/Subscriber.java.jinja | 83 + .../src/generate/java/Timestamped.java.jinja | 40 + ntcore/src/generate/java/Topic.java.jinja | 222 ++ ntcore/src/generate/types.json | 366 +++ .../networktables/ConnectionListener.java | 141 + .../ConnectionListenerPoller.java | 78 + .../networktables/ConnectionNotification.java | 5 +- .../wpi/first/networktables/EntryBase.java | 44 + .../wpi/first/networktables/EntryInfo.java | 62 - .../networktables/EntryListenerFlags.java | 66 - .../networktables/EntryNotification.java | 69 - .../wpi/first/networktables/GenericEntry.java | 15 + .../first/networktables/MultiSubscriber.java | 61 + .../networktables/NTSendableBuilder.java | 16 +- .../wpi/first/networktables/NetworkTable.java | 322 ++- .../networktables/NetworkTableEntry.java | 858 ------ .../networktables/NetworkTableInstance.java | 1194 -------- .../first/networktables/NetworkTableType.java | 115 +- .../networktables/NetworkTableValue.java | 518 ---- .../first/networktables/NetworkTablesJNI.java | 292 -- .../networktables/PersistentException.java | 16 - .../edu/wpi/first/networktables/PubSub.java | 32 + .../wpi/first/networktables/PubSubOption.java | 78 + .../wpi/first/networktables/Publisher.java} | 11 +- .../wpi/first/networktables/RpcAnswer.java | 102 - .../edu/wpi/first/networktables/RpcCall.java | 90 - .../wpi/first/networktables/Subscriber.java | 22 + .../networktables/TableEntryListener.java | 22 - .../edu/wpi/first/networktables/Topic.java | 319 +++ .../wpi/first/networktables/TopicInfo.java | 56 + .../first/networktables/TopicListener.java | 238 ++ .../networktables/TopicListenerFlags.java | 47 + .../networktables/TopicListenerPoller.java | 124 + .../networktables/TopicNotification.java | 37 + .../first/networktables/ValueListener.java | 189 ++ .../networktables/ValueListenerFlags.java | 34 + .../networktables/ValueListenerPoller.java | 101 + .../networktables/ValueNotification.java | 73 + ntcore/src/main/native/cpp/ConnectionList.cpp | 338 +++ ntcore/src/main/native/cpp/ConnectionList.h | 56 + .../main/native/cpp/ConnectionNotifier.cpp | 32 - .../src/main/native/cpp/ConnectionNotifier.h | 77 - ntcore/src/main/native/cpp/Dispatcher.cpp | 765 ----- ntcore/src/main/native/cpp/Dispatcher.h | 168 -- ntcore/src/main/native/cpp/DsClient.cpp | 177 -- ntcore/src/main/native/cpp/DsClient.h | 33 - ntcore/src/main/native/cpp/EntryNotifier.cpp | 109 - ntcore/src/main/native/cpp/EntryNotifier.h | 112 - ntcore/src/main/native/cpp/Handle.h | 23 +- ntcore/src/main/native/cpp/HandleMap.h | 54 + ntcore/src/main/native/cpp/IConnectionList.h | 24 + .../src/main/native/cpp/IConnectionNotifier.h | 30 - ntcore/src/main/native/cpp/IDispatcher.h | 31 - ntcore/src/main/native/cpp/IEntryNotifier.h | 43 - ntcore/src/main/native/cpp/INetworkClient.h | 32 + .../src/main/native/cpp/INetworkConnection.h | 38 - ntcore/src/main/native/cpp/IRpcServer.h | 36 - ntcore/src/main/native/cpp/IStorage.h | 62 - ntcore/src/main/native/cpp/InstanceImpl.cpp | 102 +- ntcore/src/main/native/cpp/InstanceImpl.h | 55 +- ntcore/src/main/native/cpp/LocalStorage.cpp | 2489 +++++++++++++++++ ntcore/src/main/native/cpp/LocalStorage.h | 250 ++ ntcore/src/main/native/cpp/Log.h | 5 +- ntcore/src/main/native/cpp/LoggerImpl.cpp | 127 +- ntcore/src/main/native/cpp/LoggerImpl.h | 107 +- ntcore/src/main/native/cpp/Message.cpp | 375 --- ntcore/src/main/native/cpp/Message.h | 114 - ntcore/src/main/native/cpp/NetworkClient.cpp | 530 ++++ ntcore/src/main/native/cpp/NetworkClient.h | 68 + .../src/main/native/cpp/NetworkConnection.cpp | 380 --- .../src/main/native/cpp/NetworkConnection.h | 124 - ntcore/src/main/native/cpp/NetworkServer.cpp | 556 ++++ ntcore/src/main/native/cpp/NetworkServer.h | 42 + ntcore/src/main/native/cpp/PubSubOptions.cpp | 36 + ntcore/src/main/native/cpp/PubSubOptions.h | 27 + ntcore/src/main/native/cpp/RpcServer.cpp | 51 - ntcore/src/main/native/cpp/RpcServer.h | 114 - ntcore/src/main/native/cpp/Storage.cpp | 1514 ---------- ntcore/src/main/native/cpp/Storage.h | 304 -- ntcore/src/main/native/cpp/Storage_load.cpp | 484 ---- ntcore/src/main/native/cpp/Storage_save.cpp | 283 -- ntcore/src/main/native/cpp/Types_internal.cpp | 83 + ntcore/src/main/native/cpp/Types_internal.h | 17 + ntcore/src/main/native/cpp/Value.cpp | 273 +- ntcore/src/main/native/cpp/Value_internal.h | 42 +- ntcore/src/main/native/cpp/WireDecoder.cpp | 247 -- ntcore/src/main/native/cpp/WireDecoder.h | 165 -- ntcore/src/main/native/cpp/WireEncoder.cpp | 226 -- ntcore/src/main/native/cpp/WireEncoder.h | 108 - .../main/native/cpp/jni/NetworkTablesJNI.cpp | 1805 +++++------- ntcore/src/main/native/cpp/net/ClientImpl.cpp | 487 ++++ ntcore/src/main/native/cpp/net/ClientImpl.h | 79 + ntcore/src/main/native/cpp/net/Message.h | 100 + .../main/native/cpp/net/NetworkInterface.h | 68 + .../main/native/cpp/net/NetworkLoopQueue.cpp | 54 + .../main/native/cpp/net/NetworkLoopQueue.h | 55 + .../main/native/cpp/net/NetworkLoopQueue.inc | 70 + ntcore/src/main/native/cpp/net/ServerImpl.cpp | 2340 ++++++++++++++++ ntcore/src/main/native/cpp/net/ServerImpl.h | 94 + .../native/cpp/net/WebSocketConnection.cpp | 122 + .../main/native/cpp/net/WebSocketConnection.h | 66 + .../src/main/native/cpp/net/WireConnection.h | 110 + .../src/main/native/cpp/net/WireDecoder.cpp | 564 ++++ ntcore/src/main/native/cpp/net/WireDecoder.h | 65 + .../src/main/native/cpp/net/WireEncoder.cpp | 316 +++ ntcore/src/main/native/cpp/net/WireEncoder.h | 62 + .../src/main/native/cpp/net3/ClientImpl3.cpp | 672 +++++ ntcore/src/main/native/cpp/net3/ClientImpl3.h | 73 + ntcore/src/main/native/cpp/net3/Message3.h | 155 + .../native/cpp/{ => net3}/SequenceNumber.cpp | 4 +- .../native/cpp/{ => net3}/SequenceNumber.h | 9 +- .../native/cpp/net3/UvStreamConnection3.cpp | 50 + .../native/cpp/net3/UvStreamConnection3.h | 57 + .../main/native/cpp/net3/WireConnection3.h | 65 + .../src/main/native/cpp/net3/WireDecoder3.cpp | 600 ++++ .../src/main/native/cpp/net3/WireDecoder3.h | 65 + .../src/main/native/cpp/net3/WireEncoder3.cpp | 327 +++ .../src/main/native/cpp/net3/WireEncoder3.h | 50 + .../native/cpp/networktables/NetworkTable.cpp | 211 +- .../cpp/networktables/NetworkTableEntry.cpp | 5 + .../networktables/NetworkTableInstance.cpp | 90 +- .../main/native/cpp/networktables/Topic.cpp | 58 + ntcore/src/main/native/cpp/ntcore_c.cpp | 1159 +++----- ntcore/src/main/native/cpp/ntcore_cpp.cpp | 1515 ++++------ ntcore/src/main/native/cpp/ntcore_test.cpp | 107 +- .../networktables/ConnectionListener.h | 116 + .../networktables/ConnectionListener.inc | 77 + .../networktables/EntryListenerFlags.h | 75 - .../include/networktables/GenericEntry.h | 482 ++++ .../include/networktables/GenericEntry.inc | 213 ++ .../include/networktables/MultiSubscriber.h | 62 + .../include/networktables/MultiSubscriber.inc | 36 + .../include/networktables/NTSendableBuilder.h | 22 +- .../include/networktables/NetworkTable.h | 212 +- .../include/networktables/NetworkTableEntry.h | 342 +-- .../networktables/NetworkTableEntry.inc | 302 +- .../networktables/NetworkTableInstance.h | 475 ++-- .../networktables/NetworkTableInstance.inc | 139 +- .../include/networktables/NetworkTableType.h | 10 +- .../include/networktables/NetworkTableValue.h | 343 ++- .../native/include/networktables/RpcCall.h | 106 - .../native/include/networktables/RpcCall.inc | 51 - .../networktables/TableEntryListener.h | 38 - .../include/networktables/TableListener.h | 33 - .../main/native/include/networktables/Topic.h | 384 +++ .../native/include/networktables/Topic.inc | 109 + .../include/networktables/TopicListener.h | 246 ++ .../include/networktables/TopicListener.inc | 116 + .../include/networktables/ValueListener.h | 204 ++ .../include/networktables/ValueListener.inc | 103 + ntcore/src/main/native/include/ntcore.h | 5 +- ntcore/src/main/native/include/ntcore_c.h | 1792 ++++++------ ntcore/src/main/native/include/ntcore_cpp.h | 1309 +++++---- ntcore/src/main/native/include/ntcore_test.h | 5 +- .../networktables/ConnectionListenerTest.java | 88 +- .../wpi/first/networktables/LoggerTest.java | 10 +- ...stenerTest.java => TopicListenerTest.java} | 45 +- .../native/cpp/ConnectionListenerTest.cpp | 82 +- .../src/test/native/cpp/EntryListenerTest.cpp | 165 -- .../src/test/native/cpp/EntryNotifierTest.cpp | 312 --- .../src/test/native/cpp/LocalStorageTest.cpp | 469 ++++ ntcore/src/test/native/cpp/MessageMatcher.h | 40 - .../src/test/native/cpp/MockConnectionList.h | 24 + .../test/native/cpp/MockConnectionNotifier.h | 27 - ntcore/src/test/native/cpp/MockDispatcher.h | 24 - .../src/test/native/cpp/MockEntryNotifier.h | 40 - ntcore/src/test/native/cpp/MockLogger.h | 24 + .../test/native/cpp/MockNetworkConnection.h | 31 - ntcore/src/test/native/cpp/MockRpcServer.h | 26 - .../test/native/cpp/PubSubOptionsMatcher.cpp | 42 + .../test/native/cpp/PubSubOptionsMatcher.h | 34 + ntcore/src/test/native/cpp/SpanMatcher.h | 73 + ntcore/src/test/native/cpp/StorageTest.cpp | 1003 ------- ntcore/src/test/native/cpp/StorageTest.h | 11 +- ntcore/src/test/native/cpp/TestPrinters.cpp | 77 +- ntcore/src/test/native/cpp/TestPrinters.h | 65 +- .../src/test/native/cpp/TopicListenerTest.cpp | 286 ++ .../src/test/native/cpp/ValueListenerTest.cpp | 280 ++ ntcore/src/test/native/cpp/ValueMatcher.cpp | 5 +- ntcore/src/test/native/cpp/ValueMatcher.h | 18 +- ntcore/src/test/native/cpp/ValueTest.cpp | 176 +- .../src/test/native/cpp/WireDecoderTest.cpp | 647 ----- .../src/test/native/cpp/WireEncoderTest.cpp | 500 ---- ntcore/src/test/native/cpp/main.cpp | 12 + .../native/cpp/net/MockNetworkInterface.h | 86 + .../native/cpp/net/MockWireConnection.cpp | 26 + .../test/native/cpp/net/MockWireConnection.h | 54 + .../test/native/cpp/net/WireDecoderTest.cpp | 214 ++ .../test/native/cpp/net/WireEncoderTest.cpp | 292 ++ .../MessageMatcher3.cpp} | 26 +- .../test/native/cpp/net3/MessageMatcher3.h | 34 + .../native/cpp/net3/MockWireConnection3.h | 44 + .../test/native/cpp/net3/WireDecoder3Test.cpp | 276 ++ .../test/native/cpp/net3/WireEncoder3Test.cpp | 283 ++ outlineviewer/build.gradle | 2 +- outlineviewer/src/main/native/cpp/main.cpp | 9 +- shared/jni/setupBuild.gradle | 9 + simulation/halsim_gui/build.gradle | 2 +- .../main/native/cpp/NetworkTablesSimGui.cpp | 18 + styleguide/spotbugs-exclude.xml | 15 + wpilibNewCommands/build.gradle | 4 +- .../wpilibj2/command/CommandScheduler.java | 43 +- .../command/button/NetworkButton.java | 35 +- .../cpp/frc2/command/CommandScheduler.cpp | 58 +- .../cpp/frc2/command/button/NetworkButton.cpp | 18 +- .../frc2/command/button/NetworkButton.h | 22 +- .../command/button/NetworkButtonTest.java | 16 +- .../frc2/command/button/NetworkButtonTest.cpp | 21 +- wpilibc/build.gradle | 8 +- wpilibc/src/main/native/cpp/ADIS16448_IMU.cpp | 6 +- wpilibc/src/main/native/cpp/ADIS16470_IMU.cpp | 6 +- wpilibc/src/main/native/cpp/ADXL345_I2C.cpp | 19 +- wpilibc/src/main/native/cpp/ADXL345_SPI.cpp | 19 +- wpilibc/src/main/native/cpp/ADXL362.cpp | 19 +- wpilibc/src/main/native/cpp/DriverStation.cpp | 56 +- .../main/native/cpp/IterativeRobotBase.cpp | 2 +- wpilibc/src/main/native/cpp/Preferences.cpp | 52 +- .../native/cpp/event/NetworkBooleanEvent.cpp | 40 + .../main/native/cpp/livewindow/LiveWindow.cpp | 17 +- .../cpp/shuffleboard/RecordingController.cpp | 21 +- .../ShuffleboardComponentBase.cpp | 22 +- .../shuffleboard/ShuffleboardContainer.cpp | 164 +- .../cpp/shuffleboard/ShuffleboardInstance.cpp | 6 +- .../native/cpp/shuffleboard/SimpleWidget.cpp | 12 +- .../native/cpp/smartdashboard/Field2d.cpp | 5 +- .../cpp/smartdashboard/FieldObject2d.cpp | 94 +- .../native/cpp/smartdashboard/Mechanism2d.cpp | 15 +- .../smartdashboard/MechanismLigament2d.cpp | 49 +- .../cpp/smartdashboard/MechanismRoot2d.cpp | 12 +- .../smartdashboard/SendableBuilderImpl.cpp | 457 ++- .../smartdashboard/SendableChooserBase.cpp | 6 +- .../cpp/smartdashboard/SmartDashboard.cpp | 31 +- wpilibc/src/main/native/cppcs/RobotBase.cpp | 20 +- .../native/include/frc/IterativeRobotBase.h | 4 +- .../include/frc/event/NetworkBooleanEvent.h | 79 + .../frc/shuffleboard/RecordingController.h | 6 +- .../frc/shuffleboard/SendableCameraWrapper.h | 31 +- .../shuffleboard/ShuffleboardComponentBase.h | 4 +- .../frc/shuffleboard/ShuffleboardContainer.h | 200 +- .../include/frc/shuffleboard/SimpleWidget.h | 17 +- .../frc/shuffleboard/SuppliedValueWidget.h | 23 +- .../frc/smartdashboard/FieldObject2d.h | 4 +- .../include/frc/smartdashboard/Mechanism2d.h | 6 +- .../frc/smartdashboard/MechanismLigament2d.h | 13 +- .../frc/smartdashboard/MechanismRoot2d.h | 6 +- .../frc/smartdashboard/SendableBuilderImpl.h | 131 +- .../frc/smartdashboard/SendableChooser.inc | 17 +- .../frc/smartdashboard/SendableChooserBase.h | 6 +- .../frc/smartdashboard/SmartDashboard.h | 48 +- .../cpp/event/NetworkBooleanEventTest.cpp | 46 + .../shuffleboard/ShuffleboardInstanceTest.cpp | 36 +- .../shuffleboard/SuppliedValueWidgetTest.cpp | 18 +- wpilibcExamples/build.gradle | 8 +- .../cpp/examples/ShuffleBoard/cpp/Robot.cpp | 14 +- .../cpp/Robot.cpp | 4 - .../cpp/Robot.cpp | 6 +- wpilibcIntegrationTests/build.gradle | 4 +- .../src/main/native/cpp/PreferencesTest.cpp | 116 +- wpilibj/build.gradle | 4 +- .../edu/wpi/first/wpilibj/ADXL345_I2C.java | 18 +- .../edu/wpi/first/wpilibj/ADXL345_SPI.java | 18 +- .../java/edu/wpi/first/wpilibj/ADXL362.java | 18 +- .../edu/wpi/first/wpilibj/DriverStation.java | 74 +- .../wpi/first/wpilibj/IterativeRobotBase.java | 6 +- .../edu/wpi/first/wpilibj/Preferences.java | 54 +- .../java/edu/wpi/first/wpilibj/RobotBase.java | 26 +- .../wpilibj/event/NetworkBooleanEvent.java | 71 + .../first/wpilibj/livewindow/LiveWindow.java | 32 +- .../wpilibj/shuffleboard/BuiltInWidgets.java | 2 +- .../wpilibj/shuffleboard/ContainerHelper.java | 62 +- .../shuffleboard/RecordingController.java | 20 +- .../shuffleboard/SendableCameraWrapper.java | 45 +- .../wpilibj/shuffleboard/Shuffleboard.java | 6 +- .../shuffleboard/ShuffleboardComponent.java | 8 +- .../shuffleboard/ShuffleboardContainer.java | 119 +- .../shuffleboard/ShuffleboardInstance.java | 12 +- .../shuffleboard/ShuffleboardLayout.java | 37 +- .../wpilibj/shuffleboard/ShuffleboardTab.java | 37 +- .../wpilibj/shuffleboard/SimpleWidget.java | 32 +- .../shuffleboard/SuppliedValueWidget.java | 38 +- .../first/wpilibj/smartdashboard/Field2d.java | 13 +- .../wpilibj/smartdashboard/FieldObject2d.java | 79 +- .../wpilibj/smartdashboard/Mechanism2d.java | 38 +- .../smartdashboard/MechanismLigament2d.java | 90 +- .../smartdashboard/MechanismObject2d.java | 9 +- .../smartdashboard/MechanismRoot2d.java | 39 +- .../smartdashboard/SendableBuilderImpl.java | 504 ++-- .../smartdashboard/SendableChooser.java | 25 +- .../smartdashboard/SmartDashboard.java | 66 +- .../wpi/first/wpilibj/PreferencesTest.java | 73 +- .../event/NetworkBooleanEventTest.java | 47 + .../SendableCameraWrapperTest.java | 12 +- .../ShuffleboardInstanceTest.java | 13 +- .../smartdashboard/SmartDashboardTest.java | 13 +- .../first/wpilibj/PreferencesTestDefault.ini | 8 - .../first/wpilibj/PreferencesTestDefault.json | 50 + wpilibjExamples/build.gradle | 4 +- .../wpilibj/examples/shuffleboard/Robot.java | 4 +- .../Robot.java | 4 - .../Robot.java | 4 - .../first/util/sendable/SendableBuilder.java | 53 +- .../first/util/sendable/SendableRegistry.java | 96 +- .../main/native/include/wpi/Synchronization.h | 4 +- .../include/wpi/sendable/SendableBuilder.h | 93 +- 380 files changed, 34573 insertions(+), 22095 deletions(-) delete mode 100644 glass/src/libnt/native/cpp/NetworkTablesHelper.cpp delete mode 100644 glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h create mode 100644 ntcore/doc/networktables4.adoc create mode 100644 ntcore/generate_topics.py create mode 100644 ntcore/src/generate/cpp/jni/types_jni.cpp.jinja create mode 100644 ntcore/src/generate/cpp/ntcore_c_types.cpp.jinja create mode 100644 ntcore/src/generate/cpp/ntcore_cpp_types.cpp.jinja create mode 100644 ntcore/src/generate/include/networktables/Topic.h.jinja create mode 100644 ntcore/src/generate/include/networktables/Topic.inc.jinja create mode 100644 ntcore/src/generate/include/ntcore_c_types.h.jinja create mode 100644 ntcore/src/generate/include/ntcore_cpp_types.h.jinja create mode 100644 ntcore/src/generate/java/Entry.java.jinja create mode 100644 ntcore/src/generate/java/EntryImpl.java.jinja create mode 100644 ntcore/src/generate/java/GenericEntryImpl.java.jinja create mode 100644 ntcore/src/generate/java/GenericPublisher.java.jinja create mode 100644 ntcore/src/generate/java/GenericSubscriber.java.jinja create mode 100644 ntcore/src/generate/java/NetworkTableEntry.java.jinja create mode 100644 ntcore/src/generate/java/NetworkTableInstance.java.jinja create mode 100644 ntcore/src/generate/java/NetworkTableValue.java.jinja create mode 100644 ntcore/src/generate/java/NetworkTablesJNI.java.jinja create mode 100644 ntcore/src/generate/java/Publisher.java.jinja create mode 100644 ntcore/src/generate/java/Subscriber.java.jinja create mode 100644 ntcore/src/generate/java/Timestamped.java.jinja create mode 100644 ntcore/src/generate/java/Topic.java.jinja create mode 100644 ntcore/src/generate/types.json create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/ConnectionListener.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/ConnectionListenerPoller.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/EntryBase.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/EntryListenerFlags.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/GenericEntry.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/MultiSubscriber.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableEntry.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/PersistentException.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/PubSub.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/PubSubOption.java rename ntcore/src/main/{native/cpp/networktables/RpcCall.cpp => java/edu/wpi/first/networktables/Publisher.java} (51%) delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/RpcCall.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/Subscriber.java delete mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/TableEntryListener.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/Topic.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/TopicInfo.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/TopicListener.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerFlags.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerPoller.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/TopicNotification.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/ValueListener.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerFlags.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerPoller.java create mode 100644 ntcore/src/main/java/edu/wpi/first/networktables/ValueNotification.java create mode 100644 ntcore/src/main/native/cpp/ConnectionList.cpp create mode 100644 ntcore/src/main/native/cpp/ConnectionList.h delete mode 100644 ntcore/src/main/native/cpp/ConnectionNotifier.cpp delete mode 100644 ntcore/src/main/native/cpp/ConnectionNotifier.h delete mode 100644 ntcore/src/main/native/cpp/Dispatcher.cpp delete mode 100644 ntcore/src/main/native/cpp/Dispatcher.h delete mode 100644 ntcore/src/main/native/cpp/DsClient.cpp delete mode 100644 ntcore/src/main/native/cpp/DsClient.h delete mode 100644 ntcore/src/main/native/cpp/EntryNotifier.cpp delete mode 100644 ntcore/src/main/native/cpp/EntryNotifier.h create mode 100644 ntcore/src/main/native/cpp/HandleMap.h create mode 100644 ntcore/src/main/native/cpp/IConnectionList.h delete mode 100644 ntcore/src/main/native/cpp/IConnectionNotifier.h delete mode 100644 ntcore/src/main/native/cpp/IDispatcher.h delete mode 100644 ntcore/src/main/native/cpp/IEntryNotifier.h create mode 100644 ntcore/src/main/native/cpp/INetworkClient.h delete mode 100644 ntcore/src/main/native/cpp/INetworkConnection.h delete mode 100644 ntcore/src/main/native/cpp/IRpcServer.h delete mode 100644 ntcore/src/main/native/cpp/IStorage.h create mode 100644 ntcore/src/main/native/cpp/LocalStorage.cpp create mode 100644 ntcore/src/main/native/cpp/LocalStorage.h delete mode 100644 ntcore/src/main/native/cpp/Message.cpp delete mode 100644 ntcore/src/main/native/cpp/Message.h create mode 100644 ntcore/src/main/native/cpp/NetworkClient.cpp create mode 100644 ntcore/src/main/native/cpp/NetworkClient.h delete mode 100644 ntcore/src/main/native/cpp/NetworkConnection.cpp delete mode 100644 ntcore/src/main/native/cpp/NetworkConnection.h create mode 100644 ntcore/src/main/native/cpp/NetworkServer.cpp create mode 100644 ntcore/src/main/native/cpp/NetworkServer.h create mode 100644 ntcore/src/main/native/cpp/PubSubOptions.cpp create mode 100644 ntcore/src/main/native/cpp/PubSubOptions.h delete mode 100644 ntcore/src/main/native/cpp/RpcServer.cpp delete mode 100644 ntcore/src/main/native/cpp/RpcServer.h delete mode 100644 ntcore/src/main/native/cpp/Storage.cpp delete mode 100644 ntcore/src/main/native/cpp/Storage.h delete mode 100644 ntcore/src/main/native/cpp/Storage_load.cpp delete mode 100644 ntcore/src/main/native/cpp/Storage_save.cpp create mode 100644 ntcore/src/main/native/cpp/Types_internal.cpp create mode 100644 ntcore/src/main/native/cpp/Types_internal.h delete mode 100644 ntcore/src/main/native/cpp/WireDecoder.cpp delete mode 100644 ntcore/src/main/native/cpp/WireDecoder.h delete mode 100644 ntcore/src/main/native/cpp/WireEncoder.cpp delete mode 100644 ntcore/src/main/native/cpp/WireEncoder.h create mode 100644 ntcore/src/main/native/cpp/net/ClientImpl.cpp create mode 100644 ntcore/src/main/native/cpp/net/ClientImpl.h create mode 100644 ntcore/src/main/native/cpp/net/Message.h create mode 100644 ntcore/src/main/native/cpp/net/NetworkInterface.h create mode 100644 ntcore/src/main/native/cpp/net/NetworkLoopQueue.cpp create mode 100644 ntcore/src/main/native/cpp/net/NetworkLoopQueue.h create mode 100644 ntcore/src/main/native/cpp/net/NetworkLoopQueue.inc create mode 100644 ntcore/src/main/native/cpp/net/ServerImpl.cpp create mode 100644 ntcore/src/main/native/cpp/net/ServerImpl.h create mode 100644 ntcore/src/main/native/cpp/net/WebSocketConnection.cpp create mode 100644 ntcore/src/main/native/cpp/net/WebSocketConnection.h create mode 100644 ntcore/src/main/native/cpp/net/WireConnection.h create mode 100644 ntcore/src/main/native/cpp/net/WireDecoder.cpp create mode 100644 ntcore/src/main/native/cpp/net/WireDecoder.h create mode 100644 ntcore/src/main/native/cpp/net/WireEncoder.cpp create mode 100644 ntcore/src/main/native/cpp/net/WireEncoder.h create mode 100644 ntcore/src/main/native/cpp/net3/ClientImpl3.cpp create mode 100644 ntcore/src/main/native/cpp/net3/ClientImpl3.h create mode 100644 ntcore/src/main/native/cpp/net3/Message3.h rename ntcore/src/main/native/cpp/{ => net3}/SequenceNumber.cpp (94%) rename ntcore/src/main/native/cpp/{ => net3}/SequenceNumber.h (92%) create mode 100644 ntcore/src/main/native/cpp/net3/UvStreamConnection3.cpp create mode 100644 ntcore/src/main/native/cpp/net3/UvStreamConnection3.h create mode 100644 ntcore/src/main/native/cpp/net3/WireConnection3.h create mode 100644 ntcore/src/main/native/cpp/net3/WireDecoder3.cpp create mode 100644 ntcore/src/main/native/cpp/net3/WireDecoder3.h create mode 100644 ntcore/src/main/native/cpp/net3/WireEncoder3.cpp create mode 100644 ntcore/src/main/native/cpp/net3/WireEncoder3.h create mode 100644 ntcore/src/main/native/cpp/networktables/Topic.cpp create mode 100644 ntcore/src/main/native/include/networktables/ConnectionListener.h create mode 100644 ntcore/src/main/native/include/networktables/ConnectionListener.inc delete mode 100644 ntcore/src/main/native/include/networktables/EntryListenerFlags.h create mode 100644 ntcore/src/main/native/include/networktables/GenericEntry.h create mode 100644 ntcore/src/main/native/include/networktables/GenericEntry.inc create mode 100644 ntcore/src/main/native/include/networktables/MultiSubscriber.h create mode 100644 ntcore/src/main/native/include/networktables/MultiSubscriber.inc delete mode 100644 ntcore/src/main/native/include/networktables/RpcCall.h delete mode 100644 ntcore/src/main/native/include/networktables/RpcCall.inc delete mode 100644 ntcore/src/main/native/include/networktables/TableEntryListener.h delete mode 100644 ntcore/src/main/native/include/networktables/TableListener.h create mode 100644 ntcore/src/main/native/include/networktables/Topic.h create mode 100644 ntcore/src/main/native/include/networktables/Topic.inc create mode 100644 ntcore/src/main/native/include/networktables/TopicListener.h create mode 100644 ntcore/src/main/native/include/networktables/TopicListener.inc create mode 100644 ntcore/src/main/native/include/networktables/ValueListener.h create mode 100644 ntcore/src/main/native/include/networktables/ValueListener.inc rename ntcore/src/test/java/edu/wpi/first/networktables/{EntryListenerTest.java => TopicListenerTest.java} (59%) delete mode 100644 ntcore/src/test/native/cpp/EntryListenerTest.cpp delete mode 100644 ntcore/src/test/native/cpp/EntryNotifierTest.cpp create mode 100644 ntcore/src/test/native/cpp/LocalStorageTest.cpp delete mode 100644 ntcore/src/test/native/cpp/MessageMatcher.h create mode 100644 ntcore/src/test/native/cpp/MockConnectionList.h delete mode 100644 ntcore/src/test/native/cpp/MockConnectionNotifier.h delete mode 100644 ntcore/src/test/native/cpp/MockDispatcher.h delete mode 100644 ntcore/src/test/native/cpp/MockEntryNotifier.h create mode 100644 ntcore/src/test/native/cpp/MockLogger.h delete mode 100644 ntcore/src/test/native/cpp/MockNetworkConnection.h delete mode 100644 ntcore/src/test/native/cpp/MockRpcServer.h create mode 100644 ntcore/src/test/native/cpp/PubSubOptionsMatcher.cpp create mode 100644 ntcore/src/test/native/cpp/PubSubOptionsMatcher.h create mode 100644 ntcore/src/test/native/cpp/SpanMatcher.h delete mode 100644 ntcore/src/test/native/cpp/StorageTest.cpp create mode 100644 ntcore/src/test/native/cpp/TopicListenerTest.cpp create mode 100644 ntcore/src/test/native/cpp/ValueListenerTest.cpp delete mode 100644 ntcore/src/test/native/cpp/WireDecoderTest.cpp delete mode 100644 ntcore/src/test/native/cpp/WireEncoderTest.cpp create mode 100644 ntcore/src/test/native/cpp/net/MockNetworkInterface.h create mode 100644 ntcore/src/test/native/cpp/net/MockWireConnection.cpp create mode 100644 ntcore/src/test/native/cpp/net/MockWireConnection.h create mode 100644 ntcore/src/test/native/cpp/net/WireDecoderTest.cpp create mode 100644 ntcore/src/test/native/cpp/net/WireEncoderTest.cpp rename ntcore/src/test/native/cpp/{MessageMatcher.cpp => net3/MessageMatcher3.cpp} (59%) create mode 100644 ntcore/src/test/native/cpp/net3/MessageMatcher3.h create mode 100644 ntcore/src/test/native/cpp/net3/MockWireConnection3.h create mode 100644 ntcore/src/test/native/cpp/net3/WireDecoder3Test.cpp create mode 100644 ntcore/src/test/native/cpp/net3/WireEncoder3Test.cpp create mode 100644 wpilibc/src/main/native/cpp/event/NetworkBooleanEvent.cpp create mode 100644 wpilibc/src/main/native/include/frc/event/NetworkBooleanEvent.h create mode 100644 wpilibc/src/test/native/cpp/event/NetworkBooleanEventTest.cpp create mode 100644 wpilibj/src/main/java/edu/wpi/first/wpilibj/event/NetworkBooleanEvent.java create mode 100644 wpilibj/src/test/java/edu/wpi/first/wpilibj/event/NetworkBooleanEventTest.java delete mode 100644 wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini create mode 100644 wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.json diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index 7f4dee64be..d8f1a0cfc4 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -15,11 +15,11 @@ jobs: - name: asan cmake-flags: "-DCMAKE_BUILD_TYPE=Asan" ctest-env: "" - ctest-flags: "-E 'ntcore|wpilibc'" + ctest-flags: "-E 'wpilibc'" - name: tsan cmake-flags: "-DCMAKE_BUILD_TYPE=Tsan" ctest-env: "TSAN_OPTIONS=second_deadlock_stack=1" - ctest-flags: "-E 'ntcore|cscore|cameraserver|wpilibc|wpilibNewCommands'" + ctest-flags: "-E 'cscore|cameraserver|wpilibc|wpilibNewCommands'" - name: ubsan cmake-flags: "-DCMAKE_BUILD_TYPE=Ubsan" ctest-env: "" diff --git a/cameraserver/build.gradle b/cameraserver/build.gradle index 11279bcf72..3f4b208356 100644 --- a/cameraserver/build.gradle +++ b/cameraserver/build.gradle @@ -57,7 +57,7 @@ model { if (!it.buildable || !(it instanceof NativeBinarySpec)) { return } - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') lib project: ':cscore', library: 'cscore', linkage: 'shared' lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' diff --git a/cameraserver/multiCameraServer/build.gradle b/cameraserver/multiCameraServer/build.gradle index 02e5f6dca3..4aafd266c7 100644 --- a/cameraserver/multiCameraServer/build.gradle +++ b/cameraserver/multiCameraServer/build.gradle @@ -55,7 +55,7 @@ model { } binaries.all { binary -> lib project: ':cameraserver', library: 'cameraserver', linkage: 'static' - lib project: ':ntcore', library: 'ntcore', linkage: 'static' + project(':ntcore').addNtcoreDependency(binary, 'static') lib project: ':cscore', library: 'cscore', linkage: 'static' lib project: ':wpinet', library: 'wpinet', linkage: 'static' lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' diff --git a/cameraserver/multiCameraServer/src/main/java/edu/wpi/Main.java b/cameraserver/multiCameraServer/src/main/java/edu/wpi/Main.java index 622b939129..69261103d1 100644 --- a/cameraserver/multiCameraServer/src/main/java/edu/wpi/Main.java +++ b/cameraserver/multiCameraServer/src/main/java/edu/wpi/Main.java @@ -175,7 +175,8 @@ public final class Main { ntinst.startServer(); } else { System.out.println("Setting up NetworkTables client for team " + team); - ntinst.startClientTeam(team); + ntinst.setServerTeam(team); + ntinst.startClient4(); } // start cameras diff --git a/cameraserver/multiCameraServer/src/main/native/cpp/main.cpp b/cameraserver/multiCameraServer/src/main/native/cpp/main.cpp index 45b56d4de5..e816b09ce5 100644 --- a/cameraserver/multiCameraServer/src/main/native/cpp/main.cpp +++ b/cameraserver/multiCameraServer/src/main/native/cpp/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -182,7 +183,8 @@ int main(int argc, char* argv[]) { ntinst.StartServer(); } else { fmt::print("Setting up NetworkTables client for team {}\n", team); - ntinst.StartClientTeam(team); + ntinst.StartClient4(); + ntinst.SetServerTeam(team); } // start cameras diff --git a/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServer.java b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServer.java index 4ad1b87623..d4a1dd64ca 100644 --- a/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServer.java +++ b/cameraserver/src/main/java/edu/wpi/first/cameraserver/CameraServer.java @@ -15,13 +15,18 @@ import edu.wpi.first.cscore.VideoException; import edu.wpi.first.cscore.VideoListener; import edu.wpi.first.cscore.VideoMode; import edu.wpi.first.cscore.VideoMode.PixelFormat; -import edu.wpi.first.cscore.VideoProperty; import edu.wpi.first.cscore.VideoSink; import edu.wpi.first.cscore.VideoSource; -import edu.wpi.first.networktables.EntryListenerFlags; +import edu.wpi.first.networktables.BooleanEntry; +import edu.wpi.first.networktables.BooleanPublisher; +import edu.wpi.first.networktables.IntegerEntry; +import edu.wpi.first.networktables.IntegerPublisher; import edu.wpi.first.networktables.NetworkTable; -import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.StringArrayPublisher; +import edu.wpi.first.networktables.StringArrayTopic; +import edu.wpi.first.networktables.StringEntry; +import edu.wpi.first.networktables.StringPublisher; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -38,11 +43,167 @@ public final class CameraServer { private static final String kPublishName = "/CameraPublisher"; + private static final class PropertyPublisher implements AutoCloseable { + @SuppressWarnings({"PMD.MissingBreakInSwitch", "PMD.ImplicitSwitchFallThrough", "fallthrough"}) + PropertyPublisher(NetworkTable table, VideoEvent event) { + String name; + String infoName; + if (event.name.startsWith("raw_")) { + name = "RawProperty/" + event.name; + infoName = "RawPropertyInfo/" + event.name; + } else { + name = "Property/" + event.name; + infoName = "PropertyInfo/" + event.name; + } + + try { + switch (event.propertyKind) { + case kBoolean: + m_booleanValueEntry = table.getBooleanTopic(name).getEntry(false); + m_booleanValueEntry.setDefault(event.value != 0); + break; + case kEnum: + m_choicesTopic = table.getStringArrayTopic(infoName + "/choices"); + // fall through + case kInteger: + m_integerValueEntry = table.getIntegerTopic(name).getEntry(0); + m_minPublisher = table.getIntegerTopic(infoName + "/min").publish(); + m_maxPublisher = table.getIntegerTopic(infoName + "/max").publish(); + m_stepPublisher = table.getIntegerTopic(infoName + "/step").publish(); + m_defaultPublisher = table.getIntegerTopic(infoName + "/default").publish(); + + m_integerValueEntry.setDefault(event.value); + m_minPublisher.set(CameraServerJNI.getPropertyMin(event.propertyHandle)); + m_maxPublisher.set(CameraServerJNI.getPropertyMax(event.propertyHandle)); + m_stepPublisher.set(CameraServerJNI.getPropertyStep(event.propertyHandle)); + m_defaultPublisher.set(CameraServerJNI.getPropertyDefault(event.propertyHandle)); + break; + case kString: + m_stringValueEntry = table.getStringTopic(name).getEntry(""); + m_stringValueEntry.setDefault(event.valueStr); + break; + default: + break; + } + } catch (VideoException ignored) { + // ignore + } + } + + void update(VideoEvent event) { + switch (event.propertyKind) { + case kBoolean: + if (m_booleanValueEntry != null) { + m_booleanValueEntry.set(event.value != 0); + } + break; + case kInteger: + case kEnum: + if (m_integerValueEntry != null) { + m_integerValueEntry.set(event.value); + } + break; + case kString: + if (m_stringValueEntry != null) { + m_stringValueEntry.set(event.valueStr); + } + break; + default: + break; + } + } + + @Override + public void close() throws Exception { + if (m_booleanValueEntry != null) { + m_booleanValueEntry.close(); + } + if (m_integerValueEntry != null) { + m_integerValueEntry.close(); + } + if (m_stringValueEntry != null) { + m_stringValueEntry.close(); + } + if (m_minPublisher != null) { + m_minPublisher.close(); + } + if (m_maxPublisher != null) { + m_maxPublisher.close(); + } + if (m_stepPublisher != null) { + m_stepPublisher.close(); + } + if (m_defaultPublisher != null) { + m_defaultPublisher.close(); + } + if (m_choicesPublisher != null) { + m_choicesPublisher.close(); + } + } + + BooleanEntry m_booleanValueEntry; + IntegerEntry m_integerValueEntry; + StringEntry m_stringValueEntry; + IntegerPublisher m_minPublisher; + IntegerPublisher m_maxPublisher; + IntegerPublisher m_stepPublisher; + IntegerPublisher m_defaultPublisher; + StringArrayTopic m_choicesTopic; + StringArrayPublisher m_choicesPublisher; + } + + private static final class SourcePublisher implements AutoCloseable { + SourcePublisher(NetworkTable table, int sourceHandle) { + this.m_table = table; + m_sourcePublisher = table.getStringTopic("source").publish(); + m_descriptionPublisher = table.getStringTopic("description").publish(); + m_connectedPublisher = table.getBooleanTopic("connected").publish(); + m_streamsPublisher = table.getStringArrayTopic("streams").publish(); + m_modeEntry = table.getStringTopic("mode").getEntry(""); + m_modesPublisher = table.getStringArrayTopic("modes").publish(); + + m_sourcePublisher.set(makeSourceValue(sourceHandle)); + m_descriptionPublisher.set(CameraServerJNI.getSourceDescription(sourceHandle)); + m_connectedPublisher.set(CameraServerJNI.isSourceConnected(sourceHandle)); + m_streamsPublisher.set(getSourceStreamValues(sourceHandle)); + + try { + VideoMode mode = CameraServerJNI.getSourceVideoMode(sourceHandle); + m_modeEntry.setDefault(videoModeToString(mode)); + m_modesPublisher.set(getSourceModeValues(sourceHandle)); + } catch (VideoException ignored) { + // Do nothing. Let the other event handlers update this if there is an error. + } + } + + @Override + public void close() throws Exception { + m_sourcePublisher.close(); + m_descriptionPublisher.close(); + m_connectedPublisher.close(); + m_streamsPublisher.close(); + m_modeEntry.close(); + m_modesPublisher.close(); + for (PropertyPublisher pp : m_properties.values()) { + pp.close(); + } + } + + final NetworkTable m_table; + final StringPublisher m_sourcePublisher; + final StringPublisher m_descriptionPublisher; + final BooleanPublisher m_connectedPublisher; + final StringArrayPublisher m_streamsPublisher; + final StringEntry m_modeEntry; + final StringArrayPublisher m_modesPublisher; + final Map m_properties = new HashMap<>(); + } + private static final AtomicInteger m_defaultUsbDevice = new AtomicInteger(); private static String m_primarySourceName; private static final Map m_sources = new HashMap<>(); private static final Map m_sinks = new HashMap<>(); - private static final Map m_tables = + private static final Map m_publishers = new HashMap<>(); // indexed by source handle // source handle indexed by sink handle private static final Map m_fixedSources = new HashMap<>(); @@ -61,189 +222,124 @@ public final class CameraServer { // - "PropertyInfo/{Property}" - Property supporting information // Listener for video events - @SuppressWarnings("PMD.UnusedPrivateField") + @SuppressWarnings({"PMD.UnusedPrivateField", "PMD.AvoidCatchingGenericException"}) private static final VideoListener m_videoListener = new VideoListener( event -> { - switch (event.kind) { - case kSourceCreated: - { - // Create subtable for the camera - NetworkTable table = m_publishTable.getSubTable(event.name); - m_tables.put(event.sourceHandle, table); - table.getEntry("source").setString(makeSourceValue(event.sourceHandle)); - table - .getEntry("description") - .setString(CameraServerJNI.getSourceDescription(event.sourceHandle)); - table - .getEntry("connected") - .setBoolean(CameraServerJNI.isSourceConnected(event.sourceHandle)); - table - .getEntry("streams") - .setStringArray(getSourceStreamValues(event.sourceHandle)); - try { - VideoMode mode = CameraServerJNI.getSourceVideoMode(event.sourceHandle); - table.getEntry("mode").setDefaultString(videoModeToString(mode)); - table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle)); - } catch (VideoException ignored) { - // Do nothing. Let the other event handlers update this if there is an error. + synchronized (CameraServer.class) { + switch (event.kind) { + case kSourceCreated: + { + // Create subtable for the camera + NetworkTable table = m_publishTable.getSubTable(event.name); + m_publishers.put( + event.sourceHandle, new SourcePublisher(table, event.sourceHandle)); + break; } - break; - } - case kSourceDestroyed: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - table.getEntry("source").setString(""); - table.getEntry("streams").setStringArray(new String[0]); - table.getEntry("modes").setStringArray(new String[0]); - } - break; - } - case kSourceConnected: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - // update the description too (as it may have changed) - table - .getEntry("description") - .setString(CameraServerJNI.getSourceDescription(event.sourceHandle)); - table.getEntry("connected").setBoolean(true); - } - break; - } - case kSourceDisconnected: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - table.getEntry("connected").setBoolean(false); - } - break; - } - case kSourceVideoModesUpdated: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - table.getEntry("modes").setStringArray(getSourceModeValues(event.sourceHandle)); - } - break; - } - case kSourceVideoModeChanged: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - table.getEntry("mode").setString(videoModeToString(event.mode)); - } - break; - } - case kSourcePropertyCreated: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - putSourcePropertyValue(table, event, true); - } - break; - } - case kSourcePropertyValueUpdated: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - putSourcePropertyValue(table, event, false); - } - break; - } - case kSourcePropertyChoicesUpdated: - { - NetworkTable table = m_tables.get(event.sourceHandle); - if (table != null) { - try { - String[] choices = - CameraServerJNI.getEnumPropertyChoices(event.propertyHandle); - table - .getEntry("PropertyInfo/" + event.name + "/choices") - .setStringArray(choices); - } catch (VideoException ignored) { - // ignore + case kSourceDestroyed: + { + SourcePublisher publisher = m_publishers.remove(event.sourceHandle); + if (publisher != null) { + try { + publisher.close(); + } catch (Exception e) { + // ignore (nothing we can do about it) + } } + break; } + case kSourceConnected: + { + SourcePublisher publisher = m_publishers.get(event.sourceHandle); + if (publisher != null) { + // update the description too (as it may have changed) + publisher.m_descriptionPublisher.set( + CameraServerJNI.getSourceDescription(event.sourceHandle)); + publisher.m_connectedPublisher.set(true); + } + break; + } + case kSourceDisconnected: + { + SourcePublisher publisher = m_publishers.get(event.sourceHandle); + if (publisher != null) { + publisher.m_connectedPublisher.set(false); + } + break; + } + case kSourceVideoModesUpdated: + { + SourcePublisher publisher = m_publishers.get(event.sourceHandle); + if (publisher != null) { + publisher.m_modesPublisher.set(getSourceModeValues(event.sourceHandle)); + } + break; + } + case kSourceVideoModeChanged: + { + SourcePublisher publisher = m_publishers.get(event.sourceHandle); + if (publisher != null) { + publisher.m_modeEntry.set(videoModeToString(event.mode)); + } + break; + } + case kSourcePropertyCreated: + { + SourcePublisher publisher = m_publishers.get(event.sourceHandle); + if (publisher != null) { + publisher.m_properties.put( + event.propertyHandle, new PropertyPublisher(publisher.m_table, event)); + } + break; + } + case kSourcePropertyValueUpdated: + { + SourcePublisher publisher = m_publishers.get(event.sourceHandle); + if (publisher != null) { + PropertyPublisher pp = publisher.m_properties.get(event.propertyHandle); + if (pp != null) { + pp.update(event); + } + } + break; + } + case kSourcePropertyChoicesUpdated: + { + SourcePublisher publisher = m_publishers.get(event.sourceHandle); + if (publisher != null) { + PropertyPublisher pp = publisher.m_properties.get(event.propertyHandle); + if (pp != null && pp.m_choicesTopic != null) { + try { + String[] choices = + CameraServerJNI.getEnumPropertyChoices(event.propertyHandle); + if (pp.m_choicesPublisher == null) { + pp.m_choicesPublisher = pp.m_choicesTopic.publish(); + } + pp.m_choicesPublisher.set(choices); + } catch (VideoException ignored) { + // ignore (just don't publish choices if we can't get them) + } + } + } + break; + } + case kSinkSourceChanged: + case kSinkCreated: + case kSinkDestroyed: + case kNetworkInterfacesChanged: + { + m_addresses = CameraServerJNI.getNetworkInterfaces(); + updateStreamValues(); + break; + } + default: break; - } - case kSinkSourceChanged: - case kSinkCreated: - case kSinkDestroyed: - case kNetworkInterfacesChanged: - { - m_addresses = CameraServerJNI.getNetworkInterfaces(); - updateStreamValues(); - break; - } - default: - break; + } } }, 0x4fff, true); - @SuppressWarnings("PMD.UnusedPrivateField") - private static final int m_tableListener = - NetworkTableInstance.getDefault() - .addEntryListener( - kPublishName + "/", - event -> { - String relativeKey = event.name.substring(kPublishName.length() + 1); - - // get source (sourceName/...) - int subKeyIndex = relativeKey.indexOf('/'); - if (subKeyIndex == -1) { - return; - } - String sourceName = relativeKey.substring(0, subKeyIndex); - VideoSource source = m_sources.get(sourceName); - if (source == null) { - return; - } - - // get subkey - relativeKey = relativeKey.substring(subKeyIndex + 1); - - // handle standard names - String propName; - if ("mode".equals(relativeKey)) { - // reset to current mode - event.getEntry().setString(videoModeToString(source.getVideoMode())); - return; - } else if (relativeKey.startsWith("Property/")) { - propName = relativeKey.substring(9); - } else if (relativeKey.startsWith("RawProperty/")) { - propName = relativeKey.substring(12); - } else { - return; // ignore - } - - // everything else is a property - VideoProperty property = source.getProperty(propName); - switch (property.getKind()) { - case kNone: - return; - case kBoolean: - // reset to current setting - event.getEntry().setBoolean(property.get() != 0); - return; - case kInteger: - case kEnum: - // reset to current setting - event.getEntry().setDouble(property.get()); - return; - case kString: - // reset to current setting - event.getEntry().setString(property.getString()); - return; - default: - return; - } - }, - EntryListenerFlags.kImmediate | EntryListenerFlags.kUpdate); - private static int m_nextPort = kBasePort; private static String[] m_addresses = new String[0]; @@ -369,8 +465,8 @@ public final class CameraServer { if (source == 0) { continue; } - NetworkTable table = m_tables.get(source); - if (table != null) { + SourcePublisher publisher = m_publishers.get(source); + if (publisher != null) { // Don't set stream values if this is a HttpCamera passthrough if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source)) == VideoSource.Kind.kHttp) { @@ -380,7 +476,7 @@ public final class CameraServer { // Set table value String[] values = getSinkStreamValues(sink); if (values.length > 0) { - table.getEntry("streams").setStringArray(values); + publisher.m_streamsPublisher.set(values); } } } @@ -390,12 +486,12 @@ public final class CameraServer { int source = i.getHandle(); // Get the source's subtable (if none exists, we're done) - NetworkTable table = m_tables.get(source); - if (table != null) { + SourcePublisher publisher = m_publishers.get(source); + if (publisher != null) { // Set table value String[] values = getSourceStreamValues(source); if (values.length > 0) { - table.getEntry("streams").setStringArray(values); + publisher.m_streamsPublisher.set(values); } } } @@ -449,69 +545,6 @@ public final class CameraServer { return modeStrings; } - /** - * Publish a source property value to NetworkTables. - * - * @param table NetworkTable to which to push value. - * @param event Video event. - * @param isNew Whether the property value hasn't been pushed to NetworkTables before. - */ - private static void putSourcePropertyValue(NetworkTable table, VideoEvent event, boolean isNew) { - String name; - String infoName; - if (event.name.startsWith("raw_")) { - name = "RawProperty/" + event.name; - infoName = "RawPropertyInfo/" + event.name; - } else { - name = "Property/" + event.name; - infoName = "PropertyInfo/" + event.name; - } - - NetworkTableEntry entry = table.getEntry(name); - try { - switch (event.propertyKind) { - case kBoolean: - if (isNew) { - entry.setDefaultBoolean(event.value != 0); - } else { - entry.setBoolean(event.value != 0); - } - break; - case kInteger: - case kEnum: - if (isNew) { - entry.setDefaultDouble(event.value); - table - .getEntry(infoName + "/min") - .setDouble(CameraServerJNI.getPropertyMin(event.propertyHandle)); - table - .getEntry(infoName + "/max") - .setDouble(CameraServerJNI.getPropertyMax(event.propertyHandle)); - table - .getEntry(infoName + "/step") - .setDouble(CameraServerJNI.getPropertyStep(event.propertyHandle)); - table - .getEntry(infoName + "/default") - .setDouble(CameraServerJNI.getPropertyDefault(event.propertyHandle)); - } else { - entry.setDouble(event.value); - } - break; - case kString: - if (isNew) { - entry.setDefaultString(event.valueStr); - } else { - entry.setString(event.valueStr); - } - break; - default: - break; - } - } catch (VideoException ignored) { - // ignore - } - } - private CameraServer() {} /** diff --git a/cameraserver/src/main/native/cpp/cameraserver/CameraServer.cpp b/cameraserver/src/main/native/cpp/cameraserver/CameraServer.cpp index 1ff6ebbe13..4fdfdb5da0 100644 --- a/cameraserver/src/main/native/cpp/cameraserver/CameraServer.cpp +++ b/cameraserver/src/main/native/cpp/cameraserver/CameraServer.cpp @@ -8,8 +8,12 @@ #include #include +#include +#include #include #include +#include +#include #include #include #include @@ -24,9 +28,42 @@ using namespace frc; static constexpr char const* kPublishName = "/CameraPublisher"; namespace { + +struct Instance; + +struct PropertyPublisher { + PropertyPublisher(nt::NetworkTable& table, const cs::VideoEvent& event); + + void Update(const cs::VideoEvent& event); + + nt::BooleanEntry booleanValueEntry; + nt::IntegerEntry integerValueEntry; + nt::StringEntry stringValueEntry; + nt::IntegerPublisher minPublisher; + nt::IntegerPublisher maxPublisher; + nt::IntegerPublisher stepPublisher; + nt::IntegerPublisher defaultPublisher; + nt::StringArrayTopic choicesTopic; + nt::StringArrayPublisher choicesPublisher; +}; + +struct SourcePublisher { + SourcePublisher(Instance& inst, std::shared_ptr table, + CS_Source source); + + std::shared_ptr table; + nt::StringPublisher sourcePublisher; + nt::StringPublisher descriptionPublisher; + nt::BooleanPublisher connectedPublisher; + nt::StringArrayPublisher streamsPublisher; + nt::StringEntry modeEntry; + nt::StringArrayPublisher modesPublisher; + wpi::DenseMap properties; +}; + struct Instance { Instance(); - std::shared_ptr GetSourceTable(CS_Source source); + SourcePublisher* GetPublisher(CS_Source source); std::vector GetSinkStreamValues(CS_Sink sink); std::vector GetSourceStreamValues(CS_Source source); void UpdateStreamValues(); @@ -37,7 +74,7 @@ struct Instance { wpi::StringMap m_sources; wpi::StringMap m_sinks; wpi::DenseMap m_fixedSources; - wpi::DenseMap> m_tables; + wpi::DenseMap m_publishers; std::shared_ptr m_publishTable{ nt::NetworkTableInstance::GetDefault().GetTable(kPublishName)}; cs::VideoListener m_videoListener; @@ -45,6 +82,7 @@ struct Instance { int m_nextPort{CameraServer::kBasePort}; std::vector m_addresses; }; + } // namespace static Instance& GetInstance() { @@ -86,9 +124,13 @@ static std::string MakeStreamValue(std::string_view address, int port) { return fmt::format("mjpg:http://{}:{}/?action=stream", address, port); } -std::shared_ptr Instance::GetSourceTable(CS_Source source) { - std::scoped_lock lock(m_mutex); - return m_tables.lookup(source); +SourcePublisher* Instance::GetPublisher(CS_Source source) { + auto it = m_publishers.find(source); + if (it != m_publishers.end()) { + return &it->second; + } else { + return nullptr; + } } std::vector Instance::GetSinkStreamValues(CS_Sink sink) { @@ -158,7 +200,6 @@ std::vector Instance::GetSourceStreamValues(CS_Source source) { } void Instance::UpdateStreamValues() { - std::scoped_lock lock(m_mutex); // Over all the sinks... for (const auto& i : m_sinks) { CS_Status status = 0; @@ -172,8 +213,7 @@ void Instance::UpdateStreamValues() { if (source == 0) { continue; } - auto table = m_tables.lookup(source); - if (table) { + if (auto publisher = GetPublisher(source)) { // Don't set stream values if this is a HttpCamera passthrough if (cs::GetSourceKind(source, &status) == CS_SOURCE_HTTP) { continue; @@ -182,7 +222,7 @@ void Instance::UpdateStreamValues() { // Set table value auto values = GetSinkStreamValues(sink); if (!values.empty()) { - table->GetEntry("streams").SetStringArray(values); + publisher->streamsPublisher.Set(values); } } } @@ -192,12 +232,11 @@ void Instance::UpdateStreamValues() { CS_Source source = i.second.GetHandle(); // Get the source's subtable (if none exists, we're done) - auto table = m_tables.lookup(source); - if (table) { + if (auto publisher = GetPublisher(source)) { // Set table value auto values = GetSourceStreamValues(source); if (!values.empty()) { - table->GetEntry("streams").SetStringArray(values); + publisher->streamsPublisher.Set(values); } } } @@ -234,51 +273,71 @@ static std::vector GetSourceModeValues(int source) { return rv; } -static void PutSourcePropertyValue(nt::NetworkTable* table, - const cs::VideoEvent& event, bool isNew) { - std::string_view namePrefix; - std::string_view infoPrefix; +PropertyPublisher::PropertyPublisher(nt::NetworkTable& table, + const cs::VideoEvent& event) { + std::string name; + std::string infoName; if (wpi::starts_with(event.name, "raw_")) { - namePrefix = "RawProperty"; - infoPrefix = "RawPropertyInfo"; + name = fmt::format("RawProperty/{}", event.name); + infoName = fmt::format("RawPropertyInfo/{}", event.name); } else { - namePrefix = "Property"; - infoPrefix = "PropertyInfo"; + name = fmt::format("Property/{}", event.name); + infoName = fmt::format("PropertyInfo/{}", event.name); } - wpi::SmallString<64> buf; CS_Status status = 0; - nt::NetworkTableEntry entry = - table->GetEntry(fmt::format("{}/{}", namePrefix, event.name)); switch (event.propertyKind) { case CS_PROP_BOOLEAN: - if (isNew) { - entry.SetDefaultBoolean(event.value != 0); - } else { - entry.SetBoolean(event.value != 0); + booleanValueEntry = table.GetBooleanTopic(name).GetEntry(false); + booleanValueEntry.SetDefault(event.value != 0); + break; + case CS_PROP_ENUM: + choicesTopic = + table.GetStringArrayTopic(fmt::format("{}/choices", infoName)); + [[fallthrough]]; + case CS_PROP_INTEGER: + integerValueEntry = table.GetIntegerTopic(name).GetEntry(0); + minPublisher = + table.GetIntegerTopic(fmt::format("{}/min", infoName)).Publish(); + maxPublisher = + table.GetIntegerTopic(fmt::format("{}/max", infoName)).Publish(); + stepPublisher = + table.GetIntegerTopic(fmt::format("{}/step", infoName)).Publish(); + defaultPublisher = + table.GetIntegerTopic(fmt::format("{}/default", infoName)).Publish(); + + integerValueEntry.SetDefault(event.value); + minPublisher.Set(cs::GetPropertyMin(event.propertyHandle, &status)); + maxPublisher.Set(cs::GetPropertyMax(event.propertyHandle, &status)); + stepPublisher.Set(cs::GetPropertyStep(event.propertyHandle, &status)); + defaultPublisher.Set( + cs::GetPropertyDefault(event.propertyHandle, &status)); + break; + case CS_PROP_STRING: + stringValueEntry = table.GetStringTopic(name).GetEntry(""); + stringValueEntry.SetDefault(event.valueStr); + break; + default: + break; + } +} + +void PropertyPublisher::Update(const cs::VideoEvent& event) { + switch (event.propertyKind) { + case CS_PROP_BOOLEAN: + if (booleanValueEntry) { + booleanValueEntry.Set(event.value != 0); } break; case CS_PROP_INTEGER: case CS_PROP_ENUM: - if (isNew) { - entry.SetDefaultDouble(event.value); - table->GetEntry(fmt::format("{}/{}/min", infoPrefix, event.name)) - .SetDouble(cs::GetPropertyMin(event.propertyHandle, &status)); - table->GetEntry(fmt::format("{}/{}/max", infoPrefix, event.name)) - .SetDouble(cs::GetPropertyMax(event.propertyHandle, &status)); - table->GetEntry(fmt::format("{}/{}/step", infoPrefix, event.name)) - .SetDouble(cs::GetPropertyStep(event.propertyHandle, &status)); - table->GetEntry(fmt::format("{}/{}/default", infoPrefix, event.name)) - .SetDouble(cs::GetPropertyDefault(event.propertyHandle, &status)); - } else { - entry.SetDouble(event.value); + if (integerValueEntry) { + integerValueEntry.Set(event.value); } break; case CS_PROP_STRING: - if (isNew) { - entry.SetDefaultString(event.valueStr); - } else { - entry.SetString(event.valueStr); + if (stringValueEntry) { + stringValueEntry.Set(event.valueStr); } break; default: @@ -286,6 +345,28 @@ static void PutSourcePropertyValue(nt::NetworkTable* table, } } +SourcePublisher::SourcePublisher(Instance& inst, + std::shared_ptr table, + CS_Source source) + : table{table}, + sourcePublisher{table->GetStringTopic("source").Publish()}, + descriptionPublisher{table->GetStringTopic("description").Publish()}, + connectedPublisher{table->GetBooleanTopic("connected").Publish()}, + streamsPublisher{table->GetStringArrayTopic("streams").Publish()}, + modeEntry{table->GetStringTopic("mode").GetEntry("")}, + modesPublisher{table->GetStringArrayTopic("modes").Publish()} { + CS_Status status = 0; + wpi::SmallString<64> buf; + sourcePublisher.Set(MakeSourceValue(source, buf)); + wpi::SmallString<64> descBuf; + descriptionPublisher.Set(cs::GetSourceDescription(source, descBuf, &status)); + connectedPublisher.Set(cs::IsSourceConnected(source, &status)); + streamsPublisher.Set(inst.GetSourceStreamValues(source)); + auto mode = cs::GetSourceVideoMode(source, &status); + modeEntry.SetDefault(VideoModeToString(mode)); + modesPublisher.Set(GetSourceModeValues(source)); +} + Instance::Instance() { // We publish sources to NetworkTables using the following structure: // "/CameraPublisher/{Source.Name}/" - root @@ -301,176 +382,87 @@ Instance::Instance() { // Listener for video events m_videoListener = cs::VideoListener{ [=](const cs::VideoEvent& event) { + std::scoped_lock lock(m_mutex); CS_Status status = 0; switch (event.kind) { case cs::VideoEvent::kSourceCreated: { // Create subtable for the camera auto table = m_publishTable->GetSubTable(event.name); - { - std::scoped_lock lock(m_mutex); - m_tables.insert(std::make_pair(event.sourceHandle, table)); - } - wpi::SmallString<64> buf; - table->GetEntry("source").SetString( - MakeSourceValue(event.sourceHandle, buf)); - wpi::SmallString<64> descBuf; - table->GetEntry("description") - .SetString(cs::GetSourceDescription(event.sourceHandle, descBuf, - &status)); - table->GetEntry("connected") - .SetBoolean(cs::IsSourceConnected(event.sourceHandle, &status)); - table->GetEntry("streams").SetStringArray( - GetSourceStreamValues(event.sourceHandle)); - auto mode = cs::GetSourceVideoMode(event.sourceHandle, &status); - table->GetEntry("mode").SetDefaultString(VideoModeToString(mode)); - table->GetEntry("modes").SetStringArray( - GetSourceModeValues(event.sourceHandle)); + m_publishers.insert( + {event.sourceHandle, + SourcePublisher{*this, table, event.sourceHandle}}); break; } - case cs::VideoEvent::kSourceDestroyed: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { - table->GetEntry("source").SetString(""); - table->GetEntry("streams").SetStringArray( - std::vector{}); - table->GetEntry("modes").SetStringArray( - std::vector{}); - } + case cs::VideoEvent::kSourceDestroyed: + m_publishers.erase(event.sourceHandle); break; - } - case cs::VideoEvent::kSourceConnected: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { + case cs::VideoEvent::kSourceConnected: + if (auto publisher = GetPublisher(event.sourceHandle)) { // update the description too (as it may have changed) wpi::SmallString<64> descBuf; - table->GetEntry("description") - .SetString(cs::GetSourceDescription(event.sourceHandle, - descBuf, &status)); - table->GetEntry("connected").SetBoolean(true); + publisher->descriptionPublisher.Set(cs::GetSourceDescription( + event.sourceHandle, descBuf, &status)); + publisher->connectedPublisher.Set(true); } break; - } - case cs::VideoEvent::kSourceDisconnected: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { - table->GetEntry("connected").SetBoolean(false); + case cs::VideoEvent::kSourceDisconnected: + if (auto publisher = GetPublisher(event.sourceHandle)) { + publisher->connectedPublisher.Set(false); } break; - } - case cs::VideoEvent::kSourceVideoModesUpdated: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { - table->GetEntry("modes").SetStringArray( + case cs::VideoEvent::kSourceVideoModesUpdated: + if (auto publisher = GetPublisher(event.sourceHandle)) { + publisher->modesPublisher.Set( GetSourceModeValues(event.sourceHandle)); } break; - } - case cs::VideoEvent::kSourceVideoModeChanged: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { - table->GetEntry("mode").SetString(VideoModeToString(event.mode)); + case cs::VideoEvent::kSourceVideoModeChanged: + if (auto publisher = GetPublisher(event.sourceHandle)) { + publisher->modeEntry.Set(VideoModeToString(event.mode)); } break; - } - case cs::VideoEvent::kSourcePropertyCreated: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { - PutSourcePropertyValue(table.get(), event, true); + case cs::VideoEvent::kSourcePropertyCreated: + if (auto publisher = GetPublisher(event.sourceHandle)) { + publisher->properties.insert( + {event.propertyHandle, + PropertyPublisher{*publisher->table, event}}); } break; - } - case cs::VideoEvent::kSourcePropertyValueUpdated: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { - PutSourcePropertyValue(table.get(), event, false); + case cs::VideoEvent::kSourcePropertyValueUpdated: + if (auto publisher = GetPublisher(event.sourceHandle)) { + auto ppIt = publisher->properties.find(event.propertyHandle); + if (ppIt != publisher->properties.end()) { + ppIt->second.Update(event); + } } break; - } - case cs::VideoEvent::kSourcePropertyChoicesUpdated: { - auto table = GetSourceTable(event.sourceHandle); - if (table) { - auto choices = - cs::GetEnumPropertyChoices(event.propertyHandle, &status); - table - ->GetEntry(fmt::format("PropertyInfo/{}/choices", event.name)) - .SetStringArray(choices); + case cs::VideoEvent::kSourcePropertyChoicesUpdated: + if (auto publisher = GetPublisher(event.sourceHandle)) { + auto ppIt = publisher->properties.find(event.propertyHandle); + if (ppIt != publisher->properties.end() && + ppIt->second.choicesTopic) { + auto choices = + cs::GetEnumPropertyChoices(event.propertyHandle, &status); + if (!ppIt->second.choicesPublisher) { + ppIt->second.choicesPublisher = + ppIt->second.choicesTopic.Publish(); + } + ppIt->second.choicesPublisher.Set(choices); + } } break; - } case cs::VideoEvent::kSinkSourceChanged: case cs::VideoEvent::kSinkCreated: case cs::VideoEvent::kSinkDestroyed: - case cs::VideoEvent::kNetworkInterfacesChanged: { + case cs::VideoEvent::kNetworkInterfacesChanged: m_addresses = cs::GetNetworkInterfaces(); UpdateStreamValues(); break; - } default: break; } }, 0x4fff, true}; - - // Listener for NetworkTable events - // We don't currently support changing settings via NT due to - // synchronization issues, so just update to current setting if someone - // else tries to change it. - wpi::SmallString<64> buf; - m_tableListener = nt::NetworkTableInstance::GetDefault().AddEntryListener( - fmt::format("{}/", kPublishName), - [=](const nt::EntryNotification& event) { - auto relativeKey = wpi::drop_front( - event.name, std::string_view{kPublishName}.size() + 1); - - // get source (sourceName/...) - auto subKeyIndex = relativeKey.find('/'); - if (subKeyIndex == std::string_view::npos) { - return; - } - auto sourceName = wpi::slice(relativeKey, 0, subKeyIndex); - auto sourceIt = m_sources.find(sourceName); - if (sourceIt == m_sources.end()) { - return; - } - - // get subkey - relativeKey.remove_prefix(subKeyIndex + 1); - - // handle standard names - std::string_view propName; - nt::NetworkTableEntry entry{event.entry}; - if (relativeKey == "mode") { - // reset to current mode - entry.SetString(VideoModeToString(sourceIt->second.GetVideoMode())); - return; - } else if (wpi::starts_with(relativeKey, "Property/")) { - propName = wpi::substr(relativeKey, 9); - } else if (wpi::starts_with(relativeKey, "RawProperty/")) { - propName = wpi::substr(relativeKey, 12); - } else { - return; // ignore - } - - // everything else is a property - auto property = sourceIt->second.GetProperty(propName); - switch (property.GetKind()) { - case cs::VideoProperty::kNone: - return; - case cs::VideoProperty::kBoolean: - entry.SetBoolean(property.Get() != 0); - return; - case cs::VideoProperty::kInteger: - case cs::VideoProperty::kEnum: - entry.SetDouble(property.Get()); - return; - case cs::VideoProperty::kString: - entry.SetString(property.GetString()); - return; - default: - return; - } - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_UPDATE); } cs::UsbCamera CameraServer::StartAutomaticCapture() { diff --git a/docs/build.gradle b/docs/build.gradle index 122b3f6df2..691264cebc 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -198,6 +198,7 @@ task generateJavaDocs(type: Javadoc) { dependsOn project(':wpilibj').generateJavaVersion dependsOn project(':hal').generateUsageReporting dependsOn project(':wpimath').generateNat + dependsOn project(':ntcore').ntcoreGenerateJavaTypes source project(':hal').sourceSets.main.java source project(':wpiutil').sourceSets.main.java source project(':cscore').sourceSets.main.java diff --git a/glass/.styleguide b/glass/.styleguide index d555d5af8a..c2a291a78d 100644 --- a/glass/.styleguide +++ b/glass/.styleguide @@ -24,6 +24,7 @@ includeOtherLibs { ^fmt/ ^frc/ ^imgui + ^networktables/ ^ntcore ^wpi/ ^wpigui diff --git a/glass/build.gradle b/glass/build.gradle index 6c31d17234..b9e62572c4 100644 --- a/glass/build.gradle +++ b/glass/build.gradle @@ -137,7 +137,7 @@ if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxar return } lib library: nativeName, linkage: 'static' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' @@ -176,7 +176,7 @@ if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxar lib project: ':cscore', library: 'cscore', linkage: 'static' lib library: 'glassnt', linkage: 'static' lib library: nativeName, linkage: 'static' - lib project: ':ntcore', library: 'ntcore', linkage: 'static' + project(':ntcore').addNtcoreDependency(it, 'static') lib project: ':wpinet', library: 'wpinet', linkage: 'static' lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' lib project: ':wpimath', library: 'wpimath', linkage: 'static' diff --git a/glass/src/app/native/cpp/main.cpp b/glass/src/app/native/cpp/main.cpp index 03b46ec4ec..c5cc8b1b87 100644 --- a/glass/src/app/native/cpp/main.cpp +++ b/glass/src/app/native/cpp/main.cpp @@ -42,6 +42,7 @@ static std::unique_ptr gNetworkTablesModel; static std::unique_ptr gNetworkTablesSettings; static glass::LogData gNetworkTablesLog; static std::unique_ptr gNetworkTablesWindow; +static std::unique_ptr gNetworkTablesInfoWindow; static std::unique_ptr gNetworkTablesSettingsWindow; static std::unique_ptr gNetworkTablesLogWindow; @@ -78,8 +79,7 @@ static void NtInitialize() { if (!win) { return; } - bool timedOut; - for (auto&& event : nt::PollConnectionListener(poller, 0, &timedOut)) { + for (auto&& event : nt::ReadConnectionListenerQueue(poller)) { if (event.connected) { glfwSetWindowTitle( win, fmt::format("Glass - Connected ({})", event.conn.remote_ip) @@ -94,8 +94,7 @@ static void NtInitialize() { auto logPoller = nt::CreateLoggerPoller(inst); nt::AddPolledLogger(logPoller, NT_LOG_INFO, 100); gui::AddEarlyExecute([logPoller] { - bool timedOut; - for (auto&& msg : nt::PollLogger(logPoller, 0, &timedOut)) { + for (auto&& msg : nt::ReadLoggerQueue(logPoller)) { const char* level = ""; if (msg.level >= NT_LOG_CRITICAL) { level = "CRITICAL: "; @@ -132,9 +131,21 @@ static void NtInitialize() { gNetworkTablesWindow->DisableRenamePopup(); gui::AddLateExecute([] { gNetworkTablesWindow->Display(); }); + // NetworkTables info window + gNetworkTablesInfoWindow = std::make_unique( + glass::GetStorageRoot().GetChild("NetworkTables Info"), + "NetworkTables Info"); + gNetworkTablesInfoWindow->SetView(glass::MakeFunctionView( + [&] { glass::DisplayNetworkTablesInfo(gNetworkTablesModel.get()); })); + gNetworkTablesInfoWindow->SetDefaultPos(250, 130); + gNetworkTablesInfoWindow->SetDefaultSize(750, 145); + gNetworkTablesInfoWindow->SetDefaultVisibility(glass::Window::kHide); + gNetworkTablesInfoWindow->DisableRenamePopup(); + gui::AddLateExecute([] { gNetworkTablesInfoWindow->Display(); }); + // NetworkTables settings window gNetworkTablesSettings = std::make_unique( - glass::GetStorageRoot().GetChild("NetworkTables Settings")); + "glass", glass::GetStorageRoot().GetChild("NetworkTables Settings")); gui::AddEarlyExecute([] { gNetworkTablesSettings->Update(); }); gNetworkTablesSettingsWindow = std::make_unique( @@ -218,6 +229,9 @@ int main(int argc, char** argv) { if (gNetworkTablesWindow) { gNetworkTablesWindow->DisplayMenuItem("NetworkTables View"); } + if (gNetworkTablesInfoWindow) { + gNetworkTablesInfoWindow->DisplayMenuItem("NetworkTables Info"); + } if (gNetworkTablesLogWindow) { gNetworkTablesLogWindow->DisplayMenuItem("NetworkTables Log"); } diff --git a/glass/src/lib/native/cpp/other/Plot.cpp b/glass/src/lib/native/cpp/other/Plot.cpp index 569e7558a9..4550ec0775 100644 --- a/glass/src/lib/native/cpp/other/Plot.cpp +++ b/glass/src/lib/native/cpp/other/Plot.cpp @@ -83,7 +83,7 @@ class PlotSeries { return m_digital.GetValue() == kDigital || (m_digital.GetValue() == kAuto && m_source && m_source->IsDigital()); } - void AppendValue(double value, uint64_t time); + void AppendValue(double value, int64_t time); // source linkage DataSource* m_source = nullptr; @@ -248,10 +248,10 @@ void PlotSeries::SetSource(DataSource* source) { AppendValue(source->GetValue(), 0); m_newValueConn = source->valueChanged.connect_connection( - [this](double value, uint64_t time) { AppendValue(value, time); }); + [this](double value, int64_t time) { AppendValue(value, time); }); } -void PlotSeries::AppendValue(double value, uint64_t timeUs) { +void PlotSeries::AppendValue(double value, int64_t timeUs) { double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6; if (IsDigital()) { if (m_size < kMaxSize) { diff --git a/glass/src/lib/native/cpp/support/EnumSetting.cpp b/glass/src/lib/native/cpp/support/EnumSetting.cpp index 848b588120..b863b70ab8 100644 --- a/glass/src/lib/native/cpp/support/EnumSetting.cpp +++ b/glass/src/lib/native/cpp/support/EnumSetting.cpp @@ -10,16 +10,13 @@ using namespace glass; EnumSetting::EnumSetting(std::string& str, int defaultValue, std::initializer_list choices) - : m_str{str}, m_choices{choices}, m_value{defaultValue} { - // override default value if str is one of the choices - int i = 0; - for (auto choice : choices) { - if (str == choice) { - m_value = i; - break; - } - ++i; + : m_str{str}, m_choices{choices}, m_defaultValue{defaultValue} {} + +int EnumSetting::GetValue() const { + if (m_value == -1) { + UpdateValue(); } + return m_value; } void EnumSetting::SetValue(int value) { @@ -29,6 +26,9 @@ void EnumSetting::SetValue(int value) { bool EnumSetting::Combo(const char* label, int numOptions, int popup_max_height_in_items) { + if (m_value == -1) { + UpdateValue(); + } if (ImGui::Combo( label, &m_value, m_choices.data(), numOptions < 0 ? m_choices.size() : static_cast(numOptions), @@ -38,3 +38,17 @@ bool EnumSetting::Combo(const char* label, int numOptions, } return false; } + +void EnumSetting::UpdateValue() const { + // override default value if str is one of the choices + int i = 0; + for (auto choice : m_choices) { + if (m_str == choice) { + m_value = i; + return; + } + ++i; + } + // no match, default it + m_value = m_defaultValue; +} diff --git a/glass/src/lib/native/include/glass/DataSource.h b/glass/src/lib/native/include/glass/DataSource.h index 5eebb3c8a1..011155e697 100644 --- a/glass/src/lib/native/include/glass/DataSource.h +++ b/glass/src/lib/native/include/glass/DataSource.h @@ -6,13 +6,11 @@ #include -#include #include #include #include #include -#include namespace glass { @@ -38,11 +36,13 @@ class DataSource { void SetDigital(bool digital) { m_digital = digital; } bool IsDigital() const { return m_digital; } - void SetValue(double value, uint64_t time = 0) { + void SetValue(double value, int64_t time = 0) { m_value = value; + m_valueTime = time; valueChanged(value, time); } double GetValue() const { return m_value; } + int64_t GetValueTime() const { return m_valueTime; } // drag source helpers void LabelText(const char* label, const char* fmt, ...) const IM_FMTARGS(3); @@ -59,7 +59,7 @@ class DataSource { ImGuiInputTextFlags flags = 0) const; void EmitDrag(ImGuiDragDropFlags flags = 0) const; - wpi::sig::SignalBase valueChanged; + wpi::sig::Signal valueChanged; static DataSource* Find(std::string_view id); @@ -69,7 +69,8 @@ class DataSource { std::string m_id; std::string& m_name; bool m_digital = false; - std::atomic m_value = 0; + double m_value = 0; + int64_t m_valueTime = 0; }; } // namespace glass diff --git a/glass/src/lib/native/include/glass/other/StringChooser.h b/glass/src/lib/native/include/glass/other/StringChooser.h index 77f9ac2b2d..f345585d70 100644 --- a/glass/src/lib/native/include/glass/other/StringChooser.h +++ b/glass/src/lib/native/include/glass/other/StringChooser.h @@ -21,10 +21,7 @@ class StringChooserModel : public Model { virtual const std::string& GetActive() = 0; virtual const std::vector& GetOptions() = 0; - virtual void SetDefault(std::string_view val) = 0; virtual void SetSelected(std::string_view val) = 0; - virtual void SetActive(std::string_view val) = 0; - virtual void SetOptions(wpi::span val) = 0; }; void DisplayStringChooser(StringChooserModel* model); diff --git a/glass/src/lib/native/include/glass/support/EnumSetting.h b/glass/src/lib/native/include/glass/support/EnumSetting.h index c4f0c26eb6..eaf308fdb4 100644 --- a/glass/src/lib/native/include/glass/support/EnumSetting.h +++ b/glass/src/lib/native/include/glass/support/EnumSetting.h @@ -15,7 +15,7 @@ class EnumSetting { EnumSetting(std::string& str, int defaultValue, std::initializer_list choices); - int GetValue() const { return m_value; } + int GetValue() const; void SetValue(int value); // updates internal value, returns true on change @@ -23,9 +23,12 @@ class EnumSetting { int popup_max_height_in_items = -1); private: + void UpdateValue() const; + std::string& m_str; wpi::SmallVector m_choices; - int m_value; + int m_defaultValue; + mutable int m_value = -1; }; } // namespace glass diff --git a/glass/src/libnt/native/cpp/NTCommandScheduler.cpp b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp index ccc64127be..efd7be8af5 100644 --- a/glass/src/libnt/native/cpp/NTCommandScheduler.cpp +++ b/glass/src/libnt/native/cpp/NTCommandScheduler.cpp @@ -10,49 +10,38 @@ using namespace glass; NTCommandSchedulerModel::NTCommandSchedulerModel(std::string_view path) - : NTCommandSchedulerModel(nt::GetDefaultInstance(), path) {} + : NTCommandSchedulerModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTCommandSchedulerModel::NTCommandSchedulerModel(NT_Inst instance, +NTCommandSchedulerModel::NTCommandSchedulerModel(nt::NetworkTableInstance inst, std::string_view path) - : m_nt(instance), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_commands(m_nt.GetEntry(fmt::format("{}/Names", path))), - m_ids(m_nt.GetEntry(fmt::format("{}/Ids", path))), - m_cancel(m_nt.GetEntry(fmt::format("{}/Cancel", path))), - m_nameValue(wpi::rsplit(path, '/').second) { - m_nt.AddListener(m_name); - m_nt.AddListener(m_commands); - m_nt.AddListener(m_ids); - m_nt.AddListener(m_cancel); -} + : m_inst{inst}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_commands{inst.GetStringArrayTopic(fmt::format("{}/Names", path)) + .Subscribe({})}, + m_ids{ + inst.GetIntegerArrayTopic(fmt::format("{}/Ids", path)).Subscribe({})}, + m_cancel{ + inst.GetIntegerArrayTopic(fmt::format("{}/Cancel", path)).Publish()}, + m_nameValue{wpi::rsplit(path, '/').second} {} void NTCommandSchedulerModel::CancelCommand(size_t index) { if (index < m_idsValue.size()) { - nt::SetEntryValue( - m_cancel, nt::NetworkTableValue::MakeDoubleArray({m_idsValue[index]})); + m_cancel.Set({{m_idsValue[index]}}); } } void NTCommandSchedulerModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } else if (event.entry == m_commands) { - if (event.value && event.value->IsStringArray()) { - auto arr = event.value->GetStringArray(); - m_commandsValue.assign(arr.begin(), arr.end()); - } - } else if (event.entry == m_ids) { - if (event.value && event.value->IsDoubleArray()) { - auto arr = event.value->GetDoubleArray(); - m_idsValue.assign(arr.begin(), arr.end()); - } - } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_commands.ReadQueue()) { + m_commandsValue = std::move(v.value); + } + for (auto&& v : m_ids.ReadQueue()) { + m_idsValue = std::move(v.value); } } bool NTCommandSchedulerModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_commands) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_commands.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTCommandSelector.cpp b/glass/src/libnt/native/cpp/NTCommandSelector.cpp index efcbac2046..6dae15df0c 100644 --- a/glass/src/libnt/native/cpp/NTCommandSelector.cpp +++ b/glass/src/libnt/native/cpp/NTCommandSelector.cpp @@ -10,38 +10,32 @@ using namespace glass; NTCommandSelectorModel::NTCommandSelectorModel(std::string_view path) - : NTCommandSelectorModel(nt::GetDefaultInstance(), path) {} + : NTCommandSelectorModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTCommandSelectorModel::NTCommandSelectorModel(NT_Inst instance, +NTCommandSelectorModel::NTCommandSelectorModel(nt::NetworkTableInstance inst, std::string_view path) - : m_nt(instance), - m_running(m_nt.GetEntry(fmt::format("{}/running", path))), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_runningData(fmt::format("NTCmd:{}", path)), - m_nameValue(wpi::rsplit(path, '/').second) { + : m_inst{inst}, + m_running{inst.GetBooleanTopic(fmt::format("{}/running", path)) + .GetEntry(false)}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_runningData{fmt::format("NTCmd:{}", path)}, + m_nameValue{wpi::rsplit(path, '/').second} { m_runningData.SetDigital(true); - m_nt.AddListener(m_running); - m_nt.AddListener(m_name); } void NTCommandSelectorModel::SetRunning(bool run) { - nt::SetEntryValue(m_running, nt::NetworkTableValue::MakeBoolean(run)); + m_running.Set(run); } void NTCommandSelectorModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_running) { - if (event.value && event.value->IsBoolean()) { - m_runningData.SetValue(event.value->GetBoolean()); - } - } else if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } + for (auto&& v : m_running.ReadQueue()) { + m_runningData.SetValue(v.value, v.time); + } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); } } bool NTCommandSelectorModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_running) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_running.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp index b44ea07d7d..18461896e8 100644 --- a/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp +++ b/glass/src/libnt/native/cpp/NTDifferentialDrive.cpp @@ -12,46 +12,40 @@ using namespace glass; NTDifferentialDriveModel::NTDifferentialDriveModel(std::string_view path) - : NTDifferentialDriveModel(nt::GetDefaultInstance(), path) {} + : NTDifferentialDriveModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTDifferentialDriveModel::NTDifferentialDriveModel(NT_Inst instance, - std::string_view path) - : m_nt(instance), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))), - m_lPercent(m_nt.GetEntry(fmt::format("{}/Left Motor Speed", path))), - m_rPercent(m_nt.GetEntry(fmt::format("{}/Right Motor Speed", path))), - m_nameValue(wpi::rsplit(path, '/').second), - m_lPercentData(fmt::format("NTDiffDriveL:{}", path)), - m_rPercentData(fmt::format("NTDiffDriveR:{}", path)) { - m_nt.AddListener(m_name); - m_nt.AddListener(m_controllable); - m_nt.AddListener(m_lPercent); - m_nt.AddListener(m_rPercent); +NTDifferentialDriveModel::NTDifferentialDriveModel( + nt::NetworkTableInstance inst, std::string_view path) + : m_inst{inst}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path)) + .Subscribe(false)}, + m_lPercent{inst.GetDoubleTopic(fmt::format("{}/Left Motor Speed", path)) + .GetEntry(0)}, + m_rPercent{inst.GetDoubleTopic(fmt::format("{}/Right Motor Speed", path)) + .GetEntry(0)}, + m_nameValue{wpi::rsplit(path, '/').second}, + m_lPercentData{fmt::format("NTDiffDriveL:{}", path)}, + m_rPercentData{fmt::format("NTDiffDriveR:{}", path)} { + m_wheels.emplace_back("L % Output", &m_lPercentData, + [this](auto value) { m_lPercent.Set(value); }); - m_wheels.emplace_back("L % Output", &m_lPercentData, [this](auto value) { - nt::SetEntryValue(m_lPercent, nt::NetworkTableValue::MakeDouble(value)); - }); - - m_wheels.emplace_back("R % Output", &m_rPercentData, [this](auto value) { - nt::SetEntryValue(m_rPercent, nt::NetworkTableValue::MakeDouble(value)); - }); + m_wheels.emplace_back("R % Output", &m_rPercentData, + [this](auto value) { m_rPercent.Set(value); }); } void NTDifferentialDriveModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_name && event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } else if (event.entry == m_lPercent && event.value && - event.value->IsDouble()) { - m_lPercentData.SetValue(event.value->GetDouble()); - } else if (event.entry == m_rPercent && event.value && - event.value->IsDouble()) { - m_rPercentData.SetValue(event.value->GetDouble()); - } else if (event.entry == m_controllable && event.value && - event.value->IsBoolean()) { - m_controllableValue = event.value->GetBoolean(); - } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_lPercent.ReadQueue()) { + m_lPercentData.SetValue(v.value, v.time); + } + for (auto&& v : m_rPercent.ReadQueue()) { + m_rPercentData.SetValue(v.value, v.time); + } + for (auto&& v : m_controllable.ReadQueue()) { + m_controllableValue = v.value; } double l = m_lPercentData.GetValue(); @@ -62,5 +56,5 @@ void NTDifferentialDriveModel::Update() { } bool NTDifferentialDriveModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_lPercent) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_lPercent.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTDigitalInput.cpp b/glass/src/libnt/native/cpp/NTDigitalInput.cpp index 5de6c2954e..0ab8c515a4 100644 --- a/glass/src/libnt/native/cpp/NTDigitalInput.cpp +++ b/glass/src/libnt/native/cpp/NTDigitalInput.cpp @@ -10,34 +10,28 @@ using namespace glass; NTDigitalInputModel::NTDigitalInputModel(std::string_view path) - : NTDigitalInputModel{nt::GetDefaultInstance(), path} {} + : NTDigitalInputModel{nt::NetworkTableInstance::GetDefault(), path} {} -NTDigitalInputModel::NTDigitalInputModel(NT_Inst inst, std::string_view path) - : m_nt{inst}, - m_value{m_nt.GetEntry(fmt::format("{}/Value", path))}, - m_name{m_nt.GetEntry(fmt::format("{}/.name", path))}, +NTDigitalInputModel::NTDigitalInputModel(nt::NetworkTableInstance inst, + std::string_view path) + : m_inst{inst}, + m_value{inst.GetBooleanTopic(fmt::format("{}/Value", path)) + .Subscribe(false, {{nt::PubSubOption::SendAll(true)}})}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, m_valueData{fmt::format("NT_DIn:{}", path)}, m_nameValue{wpi::rsplit(path, '/').second} { - m_nt.AddListener(m_value); - m_nt.AddListener(m_name); - m_valueData.SetDigital(true); } void NTDigitalInputModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_value) { - if (event.value && event.value->IsBoolean()) { - m_valueData.SetValue(event.value->GetBoolean()); - } - } else if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } + for (auto&& v : m_value.ReadQueue()) { + m_valueData.SetValue(v.value, v.time); + } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); } } bool NTDigitalInputModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_value.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTDigitalOutput.cpp b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp index a09d424446..d09d2e5971 100644 --- a/glass/src/libnt/native/cpp/NTDigitalOutput.cpp +++ b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp @@ -9,43 +9,36 @@ using namespace glass; NTDigitalOutputModel::NTDigitalOutputModel(std::string_view path) - : NTDigitalOutputModel{nt::GetDefaultInstance(), path} {} + : NTDigitalOutputModel{nt::NetworkTableInstance::GetDefault(), path} {} -NTDigitalOutputModel::NTDigitalOutputModel(NT_Inst inst, std::string_view path) - : m_nt{inst}, - m_value{m_nt.GetEntry(fmt::format("{}/Value", path))}, - m_name{m_nt.GetEntry(fmt::format("{}/.name", path))}, - m_controllable{m_nt.GetEntry(fmt::format("{}/.controllable", path))}, +NTDigitalOutputModel::NTDigitalOutputModel(nt::NetworkTableInstance inst, + std::string_view path) + : m_inst{inst}, + m_value{inst.GetBooleanTopic(fmt::format("{}/Value", path)) + .GetEntry(false, {{nt::PubSubOption::SendAll(true)}})}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path)) + .Subscribe(false)}, m_valueData{fmt::format("NT_DOut:{}", path)} { - m_nt.AddListener(m_value); - m_nt.AddListener(m_name); - m_nt.AddListener(m_controllable); - m_valueData.SetDigital(true); } void NTDigitalOutputModel::SetValue(bool val) { - nt::SetEntryValue(m_value, nt::Value::MakeBoolean(val)); + m_value.Set(val); } void NTDigitalOutputModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_value) { - if (event.value && event.value->IsBoolean()) { - m_valueData.SetValue(event.value->GetBoolean()); - } - } else if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } else if (event.entry == m_controllable) { - if (event.value && event.value->IsBoolean()) { - m_controllableValue = event.value->GetBoolean(); - } - } + for (auto&& v : m_value.ReadQueue()) { + m_valueData.SetValue(v.value, v.time); + } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_controllable.ReadQueue()) { + m_controllableValue = v.value; } } bool NTDigitalOutputModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_value.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTFMS.cpp b/glass/src/libnt/native/cpp/NTFMS.cpp index 84c1ce78cc..7d2b55fe08 100644 --- a/glass/src/libnt/native/cpp/NTFMS.cpp +++ b/glass/src/libnt/native/cpp/NTFMS.cpp @@ -13,15 +13,19 @@ using namespace glass; NTFMSModel::NTFMSModel(std::string_view path) - : NTFMSModel{nt::GetDefaultInstance(), path} {} + : NTFMSModel{nt::NetworkTableInstance::GetDefault(), path} {} -NTFMSModel::NTFMSModel(NT_Inst inst, std::string_view path) - : m_nt{inst}, +NTFMSModel::NTFMSModel(nt::NetworkTableInstance inst, std::string_view path) + : m_inst{inst}, m_gameSpecificMessage{ - m_nt.GetEntry(fmt::format("{}/GameSpecificMessage", path))}, - m_alliance{m_nt.GetEntry(fmt::format("{}/IsRedAlliance", path))}, - m_station{m_nt.GetEntry(fmt::format("{}/StationNumber", path))}, - m_controlWord{m_nt.GetEntry(fmt::format("{}/FMSControlData", path))}, + inst.GetStringTopic(fmt::format("{}/GameSpecificMessage", path)) + .Subscribe("")}, + m_alliance{inst.GetBooleanTopic(fmt::format("{}/IsRedAlliance", path)) + .Subscribe(false, {{nt::PubSubOption::SendAll(true)}})}, + m_station{inst.GetIntegerTopic(fmt::format("{}/StationNumber", path)) + .Subscribe(0, {{nt::PubSubOption::SendAll(true)}})}, + m_controlWord{inst.GetIntegerTopic(fmt::format("{}/FMSControlData", path)) + .Subscribe(0, {{nt::PubSubOption::SendAll(true)}})}, m_fmsAttached{fmt::format("NT_FMS:FMSAttached:{}", path)}, m_dsAttached{fmt::format("NT_FMS:DSAttached:{}", path)}, m_allianceStationId{fmt::format("NT_FMS:AllianceStationID:{}", path)}, @@ -29,10 +33,6 @@ NTFMSModel::NTFMSModel(NT_Inst inst, std::string_view path) m_enabled{fmt::format("NT_FMS:RobotEnabled:{}", path)}, m_test{fmt::format("NT_FMS:TestMode:{}", path)}, m_autonomous{fmt::format("NT_FMS:AutonomousMode:{}", path)} { - m_nt.AddListener(m_alliance); - m_nt.AddListener(m_station); - m_nt.AddListener(m_controlWord); - m_fmsAttached.SetDigital(true); m_dsAttached.SetDigital(true); m_estop.SetDigital(true); @@ -43,49 +43,35 @@ NTFMSModel::NTFMSModel(NT_Inst inst, std::string_view path) std::string_view NTFMSModel::GetGameSpecificMessage( wpi::SmallVectorImpl& buf) { - buf.clear(); - auto value = nt::GetEntryValue(m_gameSpecificMessage); - if (value && value->IsString()) { - auto str = value->GetString(); - buf.append(str.begin(), str.end()); - } - return std::string_view{buf.data(), buf.size()}; + return m_gameSpecificMessage.Get(buf, ""); } void NTFMSModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_alliance) { - if (event.value && event.value->IsBoolean()) { - int allianceStationId = m_allianceStationId.GetValue(); - allianceStationId %= 3; - // true if red - allianceStationId += 3 * (event.value->GetBoolean() ? 0 : 1); - m_allianceStationId.SetValue(allianceStationId); - } - } else if (event.entry == m_station) { - if (event.value && event.value->IsDouble()) { - int allianceStationId = m_allianceStationId.GetValue(); - bool isRed = (allianceStationId < 3); - // the NT value is 1-indexed - m_allianceStationId.SetValue(event.value->GetDouble() - 1 + - 3 * (isRed ? 0 : 1)); - } - } else if (event.entry == m_controlWord) { - if (event.value && event.value->IsDouble()) { - uint32_t controlWord = event.value->GetDouble(); - // See HAL_ControlWord definition - auto time = wpi::Now(); - m_enabled.SetValue(((controlWord & 0x01) != 0) ? 1 : 0, time); - m_autonomous.SetValue(((controlWord & 0x02) != 0) ? 1 : 0, time); - m_test.SetValue(((controlWord & 0x04) != 0) ? 1 : 0, time); - m_estop.SetValue(((controlWord & 0x08) != 0) ? 1 : 0, time); - m_fmsAttached.SetValue(((controlWord & 0x10) != 0) ? 1 : 0, time); - m_dsAttached.SetValue(((controlWord & 0x20) != 0) ? 1 : 0, time); - } - } + for (auto&& v : m_alliance.ReadQueue()) { + int allianceStationId = m_allianceStationId.GetValue(); + allianceStationId %= 3; + // true if red + allianceStationId += 3 * (v.value ? 0 : 1); + m_allianceStationId.SetValue(allianceStationId, v.time); + } + for (auto&& v : m_station.ReadQueue()) { + int allianceStationId = m_allianceStationId.GetValue(); + bool isRed = (allianceStationId < 3); + // the NT value is 1-indexed + m_allianceStationId.SetValue(v.value - 1 + 3 * (isRed ? 0 : 1), v.time); + } + for (auto&& v : m_controlWord.ReadQueue()) { + uint32_t controlWord = v.value; + // See HAL_ControlWord definition + m_enabled.SetValue(((controlWord & 0x01) != 0) ? 1 : 0, v.time); + m_autonomous.SetValue(((controlWord & 0x02) != 0) ? 1 : 0, v.time); + m_test.SetValue(((controlWord & 0x04) != 0) ? 1 : 0, v.time); + m_estop.SetValue(((controlWord & 0x08) != 0) ? 1 : 0, v.time); + m_fmsAttached.SetValue(((controlWord & 0x10) != 0) ? 1 : 0, v.time); + m_dsAttached.SetValue(((controlWord & 0x20) != 0) ? 1 : 0, v.time); } } bool NTFMSModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_controlWord) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_controlWord.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTField2D.cpp b/glass/src/libnt/native/cpp/NTField2D.cpp index 47fa9a7c82..79dd12c294 100644 --- a/glass/src/libnt/native/cpp/NTField2D.cpp +++ b/glass/src/libnt/native/cpp/NTField2D.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include #include @@ -18,20 +20,16 @@ using namespace glass; class NTField2DModel::ObjectModel : public FieldObjectModel { public: - ObjectModel(std::string_view name, NT_Entry entry) - : m_name{name}, m_entry{entry} {} + ObjectModel(std::string_view name, nt::DoubleArrayTopic topic) + : m_name{name}, m_topic{topic} {} const char* GetName() const override { return m_name.c_str(); } - NT_Entry GetEntry() const { return m_entry; } + nt::DoubleArrayTopic GetTopic() const { return m_topic; } void NTUpdate(const nt::Value& value); - void Update() override { - if (auto value = nt::GetEntryValue(m_entry)) { - NTUpdate(*value); - } - } - bool Exists() override { return nt::GetEntryType(m_entry) != NT_UNASSIGNED; } + void Update() override {} + bool Exists() override { return m_topic.Exists(); } bool IsReadOnly() override { return false; } wpi::span GetPoses() override { return m_poses; } @@ -44,7 +42,8 @@ class NTField2DModel::ObjectModel : public FieldObjectModel { void UpdateNT(); std::string m_name; - NT_Entry m_entry; + nt::DoubleArrayTopic m_topic; + nt::DoubleArrayPublisher m_pub; std::vector m_poses; }; @@ -62,63 +61,21 @@ void NTField2DModel::ObjectModel::NTUpdate(const nt::Value& value) { units::meter_t{arr[i * 3 + 0]}, units::meter_t{arr[i * 3 + 1]}, frc::Rotation2d{units::degree_t{arr[i * 3 + 2]}}}; } - } else if (value.IsRaw()) { - // treat it simply as an array of doubles - std::string_view data = value.GetRaw(); - - // must be triples of doubles - auto size = data.size(); - if ((size % (3 * 8)) != 0) { - return; - } - m_poses.resize(size / (3 * 8)); - const char* p = data.data(); - for (size_t i = 0; i < size / (3 * 8); ++i) { - double x = wpi::BitsToDouble( - wpi::support::endian::readNext(p)); - double y = wpi::BitsToDouble( - wpi::support::endian::readNext(p)); - double rot = wpi::BitsToDouble( - wpi::support::endian::readNext(p)); - m_poses[i] = frc::Pose2d{units::meter_t{x}, units::meter_t{y}, - frc::Rotation2d{units::degree_t{rot}}}; - } } } void NTField2DModel::ObjectModel::UpdateNT() { - if (m_poses.size() < (255 / 3)) { - wpi::SmallVector arr; - for (auto&& pose : m_poses) { - auto& translation = pose.Translation(); - arr.push_back(translation.X().value()); - arr.push_back(translation.Y().value()); - arr.push_back(pose.Rotation().Degrees().value()); - } - nt::SetEntryTypeValue(m_entry, nt::Value::MakeDoubleArray(arr)); - } else { - // send as raw array of doubles if too big for NT array - std::vector arr; - arr.resize(m_poses.size() * 3 * 8); - char* p = arr.data(); - for (auto&& pose : m_poses) { - auto& translation = pose.Translation(); - wpi::support::endian::write64be( - p, wpi::DoubleToBits(translation.X().value())); - p += 8; - wpi::support::endian::write64be( - p, wpi::DoubleToBits(translation.Y().value())); - p += 8; - wpi::support::endian::write64be( - p, wpi::DoubleToBits(pose.Rotation().Degrees().value())); - p += 8; - } - nt::SetEntryTypeValue(m_entry, - nt::Value::MakeRaw({arr.data(), arr.size()})); + wpi::SmallVector arr; + for (auto&& pose : m_poses) { + auto& translation = pose.Translation(); + arr.push_back(translation.X().value()); + arr.push_back(translation.Y().value()); + arr.push_back(pose.Rotation().Degrees().value()); } + if (!m_pub) { + m_pub = m_topic.Publish(); + } + m_pub.Set(arr); } void NTField2DModel::ObjectModel::SetPoses(wpi::span poses) { @@ -149,69 +106,75 @@ void NTField2DModel::ObjectModel::SetRotation(size_t i, frc::Rotation2d rot) { } NTField2DModel::NTField2DModel(std::string_view path) - : NTField2DModel{nt::GetDefaultInstance(), path} {} + : NTField2DModel{nt::NetworkTableInstance::GetDefault(), path} {} -NTField2DModel::NTField2DModel(NT_Inst inst, std::string_view path) - : m_nt{inst}, - m_path{fmt::format("{}/", path)}, - m_name{m_nt.GetEntry(fmt::format("{}/.name", path))} { - m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE | - NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE); +NTField2DModel::NTField2DModel(nt::NetworkTableInstance inst, + std::string_view path) + : m_path{fmt::format("{}/", path)}, + m_inst{inst}, + m_tableSub{inst, + {{m_path}}, + {{nt::PubSubOption::SendAll(true), + nt::PubSubOption::Periodic(0.05)}}}, + m_nameTopic{inst.GetTopic(fmt::format("{}/.name", path))}, + m_topicListener{inst}, + m_valueListener{inst} { + m_topicListener.Add(m_tableSub, NT_TOPIC_NOTIFY_PUBLISH | + NT_TOPIC_NOTIFY_UNPUBLISH | + NT_TOPIC_NOTIFY_IMMEDIATE); + m_valueListener.Add(m_tableSub, + NT_VALUE_NOTIFY_IMMEDIATE | NT_VALUE_NOTIFY_LOCAL); } NTField2DModel::~NTField2DModel() = default; void NTField2DModel::Update() { - for (auto&& event : m_nt.PollListener()) { + // handle publish/unpublish + for (auto&& event : m_topicListener.ReadQueue()) { + auto name = wpi::drop_front(event.info.name, m_path.size()); + if (name.empty() || name[0] == '.') { + continue; + } + auto [it, match] = Find(event.info.name); + if (event.flags & NT_TOPIC_NOTIFY_UNPUBLISH) { + if (match) { + m_objects.erase(it); + } + continue; + } else if (event.flags & NT_TOPIC_NOTIFY_PUBLISH) { + if (!match) { + it = m_objects.emplace( + it, std::make_unique( + event.info.name, nt::DoubleArrayTopic{event.info.topic})); + } + } else if (!match) { + continue; + } + } + + // update values + for (auto&& event : m_valueListener.ReadQueue()) { // .name - if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); + if (event.topic == m_nameTopic.GetHandle()) { + if (event.value && event.value.IsString()) { + m_nameValue = event.value.GetString(); } continue; } - // common case: update of existing entry; search by entry - if (event.flags & NT_NOTIFY_UPDATE) { - auto it = std::find_if( - m_objects.begin(), m_objects.end(), - [&](const auto& e) { return e->GetEntry() == event.entry; }); - if (it != m_objects.end()) { - (*it)->NTUpdate(*event.value); - continue; - } - } - - // handle create/delete - std::string_view name = event.name; - if (wpi::starts_with(name, m_path)) { - name.remove_prefix(m_path.size()); - if (name.empty() || name[0] == '.') { - continue; - } - auto [it, match] = Find(event.name); - if (event.flags & NT_NOTIFY_DELETE) { - if (match) { - m_objects.erase(it); - } - continue; - } else if (event.flags & NT_NOTIFY_NEW) { - if (!match) { - it = m_objects.emplace( - it, std::make_unique(event.name, event.entry)); - } - } else if (!match) { - continue; - } - if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) { - (*it)->NTUpdate(*event.value); - } + auto it = + std::find_if(m_objects.begin(), m_objects.end(), [&](const auto& e) { + return e->GetTopic().GetHandle() == event.topic; + }); + if (it != m_objects.end()) { + (*it)->NTUpdate(event.value); + continue; } } } bool NTField2DModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_nameTopic.Exists(); } bool NTField2DModel::IsReadOnly() { @@ -222,8 +185,9 @@ FieldObjectModel* NTField2DModel::AddFieldObject(std::string_view name) { auto fullName = fmt::format("{}{}", m_path, name); auto [it, match] = Find(fullName); if (!match) { - it = m_objects.emplace( - it, std::make_unique(fullName, m_nt.GetEntry(fullName))); + it = m_objects.emplace(it, + std::make_unique( + fullName, m_inst.GetDoubleArrayTopic(fullName))); } return it->get(); } @@ -231,7 +195,6 @@ FieldObjectModel* NTField2DModel::AddFieldObject(std::string_view name) { void NTField2DModel::RemoveFieldObject(std::string_view name) { auto [it, match] = Find(fmt::format("{}{}", m_path, name)); if (match) { - nt::DeleteEntry((*it)->GetEntry()); m_objects.erase(it); } } diff --git a/glass/src/libnt/native/cpp/NTGyro.cpp b/glass/src/libnt/native/cpp/NTGyro.cpp index 7651d2c1ff..2ff373cb8f 100644 --- a/glass/src/libnt/native/cpp/NTGyro.cpp +++ b/glass/src/libnt/native/cpp/NTGyro.cpp @@ -10,32 +10,25 @@ using namespace glass; NTGyroModel::NTGyroModel(std::string_view path) - : NTGyroModel(nt::GetDefaultInstance(), path) {} + : NTGyroModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTGyroModel::NTGyroModel(NT_Inst instance, std::string_view path) - : m_nt(instance), - m_angle(m_nt.GetEntry(fmt::format("{}/Value", path))), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_angleData(fmt::format("NT_Gyro:{}", path)), - m_nameValue(wpi::rsplit(path, '/').second) { - m_nt.AddListener(m_angle); - m_nt.AddListener(m_name); -} +NTGyroModel::NTGyroModel(nt::NetworkTableInstance inst, std::string_view path) + : m_inst{inst}, + m_angle{inst.GetDoubleTopic(fmt::format("{}/Value", path)) + .Subscribe(0, {{nt::PubSubOption::SendAll(true)}})}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe({})}, + m_angleData{fmt::format("NT_Gyro:{}", path)}, + m_nameValue{wpi::rsplit(path, '/').second} {} void NTGyroModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_angle) { - if (event.value && event.value->IsDouble()) { - m_angleData.SetValue(event.value->GetDouble()); - } - } else if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_angle.ReadQueue()) { + m_angleData.SetValue(v.value, v.time); } } bool NTGyroModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_angle) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_angle.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTMecanumDrive.cpp b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp index 28c0a6745c..1b34c058a8 100644 --- a/glass/src/libnt/native/cpp/NTMecanumDrive.cpp +++ b/glass/src/libnt/native/cpp/NTMecanumDrive.cpp @@ -12,69 +12,62 @@ using namespace glass; NTMecanumDriveModel::NTMecanumDriveModel(std::string_view path) - : NTMecanumDriveModel(nt::GetDefaultInstance(), path) {} + : NTMecanumDriveModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTMecanumDriveModel::NTMecanumDriveModel(NT_Inst instance, +NTMecanumDriveModel::NTMecanumDriveModel(nt::NetworkTableInstance inst, std::string_view path) - : m_nt(instance), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))), - m_flPercent( - m_nt.GetEntry(fmt::format("{}/Front Left Motor Speed", path))), - m_frPercent( - m_nt.GetEntry(fmt::format("{}/Front Right Motor Speed", path))), - m_rlPercent(m_nt.GetEntry(fmt::format("{}/Rear Left Motor Speed", path))), - m_rrPercent( - m_nt.GetEntry(fmt::format("{}/Rear Right Motor Speed", path))), - m_nameValue(wpi::rsplit(path, '/').second), - m_flPercentData(fmt::format("NTMcnmDriveFL:{}", path)), - m_frPercentData(fmt::format("NTMcnmDriveFR:{}", path)), - m_rlPercentData(fmt::format("NTMcnmDriveRL:{}", path)), - m_rrPercentData(fmt::format("NTMcnmDriveRR:{}", path)) { - m_nt.AddListener(m_name); - m_nt.AddListener(m_controllable); - m_nt.AddListener(m_flPercent); - m_nt.AddListener(m_frPercent); - m_nt.AddListener(m_rlPercent); - m_nt.AddListener(m_rrPercent); + : m_inst{inst}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path)) + .Subscribe(0)}, + m_flPercent{ + inst.GetDoubleTopic(fmt::format("{}/Front Left Motor Speed", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_frPercent{ + inst.GetDoubleTopic(fmt::format("{}/Front Right Motor Speed", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_rlPercent{ + inst.GetDoubleTopic(fmt::format("{}/Rear Left Motor Speed", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_rrPercent{ + inst.GetDoubleTopic(fmt::format("{}/Rear Right Motor Speed", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_nameValue{wpi::rsplit(path, '/').second}, + m_flPercentData{fmt::format("NTMcnmDriveFL:{}", path)}, + m_frPercentData{fmt::format("NTMcnmDriveFR:{}", path)}, + m_rlPercentData{fmt::format("NTMcnmDriveRL:{}", path)}, + m_rrPercentData{fmt::format("NTMcnmDriveRR:{}", path)} { + m_wheels.emplace_back("FL % Output", &m_flPercentData, + [this](auto value) { m_flPercent.Set(value); }); - m_wheels.emplace_back("FL % Output", &m_flPercentData, [this](auto value) { - nt::SetEntryValue(m_flPercent, nt::NetworkTableValue::MakeDouble(value)); - }); + m_wheels.emplace_back("FR % Output", &m_frPercentData, + [this](auto value) { m_frPercent.Set(value); }); - m_wheels.emplace_back("FR % Output", &m_frPercentData, [this](auto value) { - nt::SetEntryValue(m_frPercent, nt::NetworkTableValue::MakeDouble(value)); - }); + m_wheels.emplace_back("RL % Output", &m_rlPercentData, + [this](auto value) { m_rlPercent.Set(value); }); - m_wheels.emplace_back("RL % Output", &m_rlPercentData, [this](auto value) { - nt::SetEntryValue(m_rlPercent, nt::NetworkTableValue::MakeDouble(value)); - }); - - m_wheels.emplace_back("RR % Output", &m_rrPercentData, [this](auto value) { - nt::SetEntryValue(m_rrPercent, nt::NetworkTableValue::MakeDouble(value)); - }); + m_wheels.emplace_back("RR % Output", &m_rrPercentData, + [this](auto value) { m_rrPercent.Set(value); }); } void NTMecanumDriveModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_name && event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } else if (event.entry == m_flPercent && event.value && - event.value->IsDouble()) { - m_flPercentData.SetValue(event.value->GetDouble()); - } else if (event.entry == m_frPercent && event.value && - event.value->IsDouble()) { - m_frPercentData.SetValue(event.value->GetDouble()); - } else if (event.entry == m_rlPercent && event.value && - event.value->IsDouble()) { - m_rlPercentData.SetValue(event.value->GetDouble()); - } else if (event.entry == m_rrPercent && event.value && - event.value->IsDouble()) { - m_rrPercentData.SetValue(event.value->GetDouble()); - } else if (event.entry == m_controllable && event.value && - event.value->IsBoolean()) { - m_controllableValue = event.value->GetBoolean(); - } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_flPercent.ReadQueue()) { + m_flPercentData.SetValue(v.value, v.time); + } + for (auto&& v : m_frPercent.ReadQueue()) { + m_frPercentData.SetValue(v.value, v.time); + } + for (auto&& v : m_rlPercent.ReadQueue()) { + m_rlPercentData.SetValue(v.value, v.time); + } + for (auto&& v : m_rrPercent.ReadQueue()) { + m_rrPercentData.SetValue(v.value, v.time); + } + for (auto&& v : m_controllable.ReadQueue()) { + m_controllableValue = v.value; } double fl = m_flPercentData.GetValue(); @@ -88,5 +81,5 @@ void NTMecanumDriveModel::Update() { } bool NTMecanumDriveModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_flPercent) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_flPercent.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTMechanism2D.cpp b/glass/src/libnt/native/cpp/NTMechanism2D.cpp index 9c73af2532..54838d92b9 100644 --- a/glass/src/libnt/native/cpp/NTMechanism2D.cpp +++ b/glass/src/libnt/native/cpp/NTMechanism2D.cpp @@ -34,16 +34,18 @@ class NTMechanismObjectModel; class NTMechanismGroupImpl final { public: - NTMechanismGroupImpl(NT_Inst inst, std::string_view path, + NTMechanismGroupImpl(nt::NetworkTableInstance inst, std::string_view path, std::string_view name) : m_inst{inst}, m_path{path}, m_name{name} {} const char* GetName() const { return m_name.c_str(); } void ForEachObject(wpi::function_ref func); - void NTUpdate(const nt::EntryNotification& event, std::string_view name); + + void NTUpdate(const nt::TopicNotification& event, std::string_view name); + void NTUpdate(const nt::ValueNotification& event, std::string_view name); protected: - NT_Inst m_inst; + nt::NetworkTableInstance m_inst; std::string m_path; std::string m_name; std::vector> m_objects; @@ -51,14 +53,14 @@ class NTMechanismGroupImpl final { class NTMechanismObjectModel final : public MechanismObjectModel { public: - NTMechanismObjectModel(NT_Inst inst, std::string_view path, + NTMechanismObjectModel(nt::NetworkTableInstance inst, std::string_view path, std::string_view name) : m_group{inst, path, name}, - m_type{nt::GetEntry(inst, fmt::format("{}/.type", path))}, - m_color{nt::GetEntry(inst, fmt::format("{}/color", path))}, - m_weight{nt::GetEntry(inst, fmt::format("{}/weight", path))}, - m_angle{nt::GetEntry(inst, fmt::format("{}/angle", path))}, - m_length{nt::GetEntry(inst, fmt::format("{}/length", path))} {} + m_typeTopic{inst.GetTopic(fmt::format("{}/.type", path))}, + m_colorTopic{inst.GetTopic(fmt::format("{}/color", path))}, + m_weightTopic{inst.GetTopic(fmt::format("{}/weight", path))}, + m_angleTopic{inst.GetTopic(fmt::format("{}/angle", path))}, + m_lengthTopic{inst.GetTopic(fmt::format("{}/length", path))} {} const char* GetName() const final { return m_group.GetName(); } void ForEachObject( @@ -72,16 +74,17 @@ class NTMechanismObjectModel final : public MechanismObjectModel { frc::Rotation2d GetAngle() const final { return m_angleValue; } units::meter_t GetLength() const final { return m_lengthValue; } - bool NTUpdate(const nt::EntryNotification& event, std::string_view childName); + bool NTUpdate(const nt::TopicNotification& event, std::string_view name); + void NTUpdate(const nt::ValueNotification& event, std::string_view name); private: NTMechanismGroupImpl m_group; - NT_Entry m_type; - NT_Entry m_color; - NT_Entry m_weight; - NT_Entry m_angle; - NT_Entry m_length; + nt::Topic m_typeTopic; + nt::Topic m_colorTopic; + nt::Topic m_weightTopic; + nt::Topic m_angleTopic; + nt::Topic m_lengthTopic; std::string m_typeValue; ImU32 m_colorValue = IM_COL32_WHITE; @@ -99,7 +102,7 @@ void NTMechanismGroupImpl::ForEachObject( } } -void NTMechanismGroupImpl::NTUpdate(const nt::EntryNotification& event, +void NTMechanismGroupImpl::NTUpdate(const nt::TopicNotification& event, std::string_view name) { if (name.empty()) { return; @@ -115,7 +118,7 @@ void NTMechanismGroupImpl::NTUpdate(const nt::EntryNotification& event, [](const auto& e, std::string_view name) { return e->GetName() < name; }); bool match = it != m_objects.end() && (*it)->GetName() == name; - if (event.flags & NT_NOTIFY_NEW) { + if (event.flags & NT_TOPIC_NOTIFY_PUBLISH) { if (!match) { it = m_objects.emplace( it, std::make_unique( @@ -123,6 +126,7 @@ void NTMechanismGroupImpl::NTUpdate(const nt::EntryNotification& event, match = true; } } + if (match) { if ((*it)->NTUpdate(event, childName)) { m_objects.erase(it); @@ -130,43 +134,74 @@ void NTMechanismGroupImpl::NTUpdate(const nt::EntryNotification& event, } } -bool NTMechanismObjectModel::NTUpdate(const nt::EntryNotification& event, +void NTMechanismGroupImpl::NTUpdate(const nt::ValueNotification& event, + std::string_view name) { + if (name.empty()) { + return; + } + std::string_view childName; + std::tie(name, childName) = wpi::split(name, '/'); + if (childName.empty()) { + return; + } + + auto it = std::lower_bound( + m_objects.begin(), m_objects.end(), name, + [](const auto& e, std::string_view name) { return e->GetName() < name; }); + if (it != m_objects.end() && (*it)->GetName() == name) { + (*it)->NTUpdate(event, childName); + } +} + +bool NTMechanismObjectModel::NTUpdate(const nt::TopicNotification& event, std::string_view childName) { - if (event.entry == m_type) { - if ((event.flags & NT_NOTIFY_DELETE) != 0) { + if (event.info.topic == m_typeTopic.GetHandle()) { + if (event.flags & NT_TOPIC_NOTIFY_UNPUBLISH) { return true; } - if (event.value && event.value->IsString()) { - m_typeValue = event.value->GetString(); - } - } else if (event.entry == m_color) { - if (event.value && event.value->IsString()) { - ConvertColor(event.value->GetString(), &m_colorValue); - } - } else if (event.entry == m_weight) { - if (event.value && event.value->IsDouble()) { - m_weightValue = event.value->GetDouble(); - } - } else if (event.entry == m_angle) { - if (event.value && event.value->IsDouble()) { - m_angleValue = units::degree_t{event.value->GetDouble()}; - } - } else if (event.entry == m_length) { - if (event.value && event.value->IsDouble()) { - m_lengthValue = units::meter_t{event.value->GetDouble()}; - } - } else { + } else if (event.info.topic != m_colorTopic.GetHandle() && + event.info.topic != m_weightTopic.GetHandle() && + event.info.topic != m_angleTopic.GetHandle() && + event.info.topic != m_lengthTopic.GetHandle()) { m_group.NTUpdate(event, childName); } return false; } +void NTMechanismObjectModel::NTUpdate(const nt::ValueNotification& event, + std::string_view childName) { + if (event.topic == m_typeTopic.GetHandle()) { + if (event.value && event.value.IsString()) { + m_typeValue = event.value.GetString(); + } + } else if (event.topic == m_colorTopic.GetHandle()) { + if (event.value && event.value.IsString()) { + ConvertColor(event.value.GetString(), &m_colorValue); + } + } else if (event.topic == m_weightTopic.GetHandle()) { + if (event.value && event.value.IsDouble()) { + m_weightValue = event.value.GetDouble(); + } + } else if (event.topic == m_angleTopic.GetHandle()) { + if (event.value && event.value.IsDouble()) { + m_angleValue = units::degree_t{event.value.GetDouble()}; + } + } else if (event.topic == m_lengthTopic.GetHandle()) { + if (event.value && event.value.IsDouble()) { + m_lengthValue = units::meter_t{event.value.GetDouble()}; + } + } else { + m_group.NTUpdate(event, childName); + } +} + class NTMechanism2DModel::RootModel final : public MechanismRootModel { public: - RootModel(NT_Inst inst, std::string_view path, std::string_view name) + RootModel(nt::NetworkTableInstance inst, std::string_view path, + std::string_view name) : m_group{inst, path, name}, - m_x{nt::GetEntry(inst, fmt::format("{}/x", path))}, - m_y{nt::GetEntry(inst, fmt::format("{}/y", path))} {} + m_xTopic{inst.GetTopic(fmt::format("{}/x", path))}, + m_yTopic{inst.GetTopic(fmt::format("{}/y", path))} {} const char* GetName() const final { return m_group.GetName(); } void ForEachObject( @@ -174,31 +209,24 @@ class NTMechanism2DModel::RootModel final : public MechanismRootModel { m_group.ForEachObject(func); } - bool NTUpdate(const nt::EntryNotification& event, std::string_view childName); + bool NTUpdate(const nt::TopicNotification& event, std::string_view childName); + void NTUpdate(const nt::ValueNotification& event, std::string_view childName); frc::Translation2d GetPosition() const override { return m_pos; }; private: NTMechanismGroupImpl m_group; - NT_Entry m_x; - NT_Entry m_y; + nt::Topic m_xTopic; + nt::Topic m_yTopic; frc::Translation2d m_pos; }; -bool NTMechanism2DModel::RootModel::NTUpdate(const nt::EntryNotification& event, +bool NTMechanism2DModel::RootModel::NTUpdate(const nt::TopicNotification& event, std::string_view childName) { - if ((event.flags & NT_NOTIFY_DELETE) != 0 && - (event.entry == m_x || event.entry == m_y)) { - return true; - } else if (event.entry == m_x) { - if (event.value && event.value->IsDouble()) { - m_pos = frc::Translation2d{units::meter_t{event.value->GetDouble()}, - m_pos.Y()}; - } - } else if (event.entry == m_y) { - if (event.value && event.value->IsDouble()) { - m_pos = frc::Translation2d{m_pos.X(), - units::meter_t{event.value->GetDouble()}}; + if (event.info.topic == m_xTopic.GetHandle() || + event.info.topic == m_yTopic.GetHandle()) { + if (event.flags & NT_TOPIC_NOTIFY_UNPUBLISH) { + return true; } } else { m_group.NTUpdate(event, childName); @@ -206,36 +234,95 @@ bool NTMechanism2DModel::RootModel::NTUpdate(const nt::EntryNotification& event, return false; } -NTMechanism2DModel::NTMechanism2DModel(std::string_view path) - : NTMechanism2DModel{nt::GetDefaultInstance(), path} {} +void NTMechanism2DModel::RootModel::NTUpdate(const nt::ValueNotification& event, + std::string_view childName) { + if (event.topic == m_xTopic.GetHandle()) { + if (event.value && event.value.IsDouble()) { + m_pos = frc::Translation2d{units::meter_t{event.value.GetDouble()}, + m_pos.Y()}; + } + } else if (event.topic == m_yTopic.GetHandle()) { + if (event.value && event.value.IsDouble()) { + m_pos = frc::Translation2d{m_pos.X(), + units::meter_t{event.value.GetDouble()}}; + } + } else { + m_group.NTUpdate(event, childName); + } +} -NTMechanism2DModel::NTMechanism2DModel(NT_Inst inst, std::string_view path) - : m_nt{inst}, +NTMechanism2DModel::NTMechanism2DModel(std::string_view path) + : NTMechanism2DModel{nt::NetworkTableInstance::GetDefault(), path} {} + +NTMechanism2DModel::NTMechanism2DModel(nt::NetworkTableInstance inst, + std::string_view path) + : m_inst{inst}, m_path{fmt::format("{}/", path)}, - m_name{m_nt.GetEntry(fmt::format("{}/.name", path))}, - m_dimensions{m_nt.GetEntry(fmt::format("{}/dims", path))}, - m_bgColor{m_nt.GetEntry(fmt::format("{}/backgroundColor", path))}, + m_tableSub{inst, + {{m_path}}, + {{nt::PubSubOption::SendAll(true), + nt::PubSubOption::Periodic(0.05)}}}, + m_nameTopic{m_inst.GetTopic(fmt::format("{}/.name", path))}, + m_dimensionsTopic{m_inst.GetTopic(fmt::format("{}/dims", path))}, + m_bgColorTopic{m_inst.GetTopic(fmt::format("{}/backgroundColor", path))}, + m_topicListener{m_inst}, + m_valueListener{m_inst}, m_dimensionsValue{1_m, 1_m} { - m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE | - NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE); + m_topicListener.Add(m_tableSub, NT_TOPIC_NOTIFY_PUBLISH | + NT_TOPIC_NOTIFY_UNPUBLISH | + NT_TOPIC_NOTIFY_IMMEDIATE); + m_valueListener.Add(m_tableSub, + NT_VALUE_NOTIFY_IMMEDIATE | NT_VALUE_NOTIFY_LOCAL); } NTMechanism2DModel::~NTMechanism2DModel() = default; void NTMechanism2DModel::Update() { - for (auto&& event : m_nt.PollListener()) { + for (auto&& event : m_topicListener.ReadQueue()) { + auto name = wpi::drop_front(event.info.name, m_path.size()); + if (name.empty() || name[0] == '.') { + continue; + } + std::string_view childName; + std::tie(name, childName) = wpi::split(name, '/'); + if (childName.empty()) { + continue; + } + + auto it = std::lower_bound(m_roots.begin(), m_roots.end(), name, + [](const auto& e, std::string_view name) { + return e->GetName() < name; + }); + bool match = it != m_roots.end() && (*it)->GetName() == name; + + if (event.flags & NT_TOPIC_NOTIFY_PUBLISH) { + if (!match) { + it = m_roots.emplace( + it, std::make_unique( + m_inst, fmt::format("{}{}", m_path, name), name)); + match = true; + } + } + if (match) { + if ((*it)->NTUpdate(event, childName)) { + m_roots.erase(it); + } + } + } + + for (auto&& event : m_valueListener.ReadQueue()) { // .name - if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); + if (event.topic == m_nameTopic.GetHandle()) { + if (event.value && event.value.IsString()) { + m_nameValue = event.value.GetString(); } continue; } // dims - if (event.entry == m_dimensions) { - if (event.value && event.value->IsDoubleArray()) { - auto arr = event.value->GetDoubleArray(); + if (event.topic == m_dimensionsTopic.GetHandle()) { + if (event.value && event.value.IsDoubleArray()) { + auto arr = event.value.GetDoubleArray(); if (arr.size() == 2) { m_dimensionsValue = frc::Translation2d{units::meter_t{arr[0]}, units::meter_t{arr[1]}}; @@ -244,50 +331,16 @@ void NTMechanism2DModel::Update() { } // backgroundColor - if (event.entry == m_bgColor) { - if (event.value && event.value->IsString()) { - ConvertColor(event.value->GetString(), &m_bgColorValue); - } - } - - std::string_view name = event.name; - if (wpi::starts_with(name, m_path)) { - name.remove_prefix(m_path.size()); - if (name.empty() || name[0] == '.') { - continue; - } - std::string_view childName; - std::tie(name, childName) = wpi::split(name, '/'); - if (childName.empty()) { - continue; - } - - auto it = std::lower_bound(m_roots.begin(), m_roots.end(), name, - [](const auto& e, std::string_view name) { - return e->GetName() < name; - }); - bool match = it != m_roots.end() && (*it)->GetName() == name; - - if (event.flags & NT_NOTIFY_NEW) { - if (!match) { - it = m_roots.emplace( - it, - std::make_unique( - m_nt.GetInstance(), fmt::format("{}{}", m_path, name), name)); - match = true; - } - } - if (match) { - if ((*it)->NTUpdate(event, childName)) { - m_roots.erase(it); - } + if (event.topic == m_bgColorTopic.GetHandle()) { + if (event.value && event.value.IsString()) { + ConvertColor(event.value.GetString(), &m_bgColorValue); } } } } bool NTMechanism2DModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_nameTopic.Exists(); } bool NTMechanism2DModel::IsReadOnly() { diff --git a/glass/src/libnt/native/cpp/NTPIDController.cpp b/glass/src/libnt/native/cpp/NTPIDController.cpp index 7936057fab..de98386409 100644 --- a/glass/src/libnt/native/cpp/NTPIDController.cpp +++ b/glass/src/libnt/native/cpp/NTPIDController.cpp @@ -10,76 +10,65 @@ using namespace glass; NTPIDControllerModel::NTPIDControllerModel(std::string_view path) - : NTPIDControllerModel(nt::GetDefaultInstance(), path) {} + : NTPIDControllerModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTPIDControllerModel::NTPIDControllerModel(NT_Inst instance, +NTPIDControllerModel::NTPIDControllerModel(nt::NetworkTableInstance inst, std::string_view path) - : m_nt(instance), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))), - m_p(m_nt.GetEntry(fmt::format("{}/p", path))), - m_i(m_nt.GetEntry(fmt::format("{}/i", path))), - m_d(m_nt.GetEntry(fmt::format("{}/d", path))), - m_setpoint(m_nt.GetEntry(fmt::format("{}/setpoint", path))), - m_pData(fmt::format("NTPIDCtrlP:{}", path)), - m_iData(fmt::format("NTPIDCtrlI:{}", path)), - m_dData(fmt::format("NTPIDCtrlD:{}", path)), - m_setpointData(fmt::format("NTPIDCtrlStpt:{}", path)), - m_nameValue(wpi::rsplit(path, '/').second) { - m_nt.AddListener(m_name); - m_nt.AddListener(m_controllable); - m_nt.AddListener(m_p); - m_nt.AddListener(m_i); - m_nt.AddListener(m_d); - m_nt.AddListener(m_setpoint); -} + : m_inst{inst}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path)) + .Subscribe(false)}, + m_p{inst.GetDoubleTopic(fmt::format("{}/p", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_i{inst.GetDoubleTopic(fmt::format("{}/i", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_d{inst.GetDoubleTopic(fmt::format("{}/d", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_setpoint{inst.GetDoubleTopic(fmt::format("{}/setpoint", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_pData{fmt::format("NTPIDCtrlP:{}", path)}, + m_iData{fmt::format("NTPIDCtrlI:{}", path)}, + m_dData{fmt::format("NTPIDCtrlD:{}", path)}, + m_setpointData{fmt::format("NTPIDCtrlStpt:{}", path)}, + m_nameValue{wpi::rsplit(path, '/').second} {} void NTPIDControllerModel::SetP(double value) { - nt::SetEntryValue(m_p, nt::NetworkTableValue::MakeDouble(value)); + m_p.Set(value); } void NTPIDControllerModel::SetI(double value) { - nt::SetEntryValue(m_i, nt::NetworkTableValue::MakeDouble(value)); + m_i.Set(value); } void NTPIDControllerModel::SetD(double value) { - nt::SetEntryValue(m_d, nt::NetworkTableValue::MakeDouble(value)); + m_d.Set(value); } void NTPIDControllerModel::SetSetpoint(double value) { - nt::SetEntryValue(m_setpoint, nt::NetworkTableValue::MakeDouble(value)); + m_setpoint.Set(value); } void NTPIDControllerModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } else if (event.entry == m_p) { - if (event.value && event.value->IsDouble()) { - m_pData.SetValue(event.value->GetDouble()); - } - } else if (event.entry == m_i) { - if (event.value && event.value->IsDouble()) { - m_iData.SetValue(event.value->GetDouble()); - } - } else if (event.entry == m_d) { - if (event.value && event.value->IsDouble()) { - m_dData.SetValue(event.value->GetDouble()); - } - } else if (event.entry == m_setpoint) { - if (event.value && event.value->IsDouble()) { - m_setpointData.SetValue(event.value->GetDouble()); - } - } else if (event.entry == m_controllable) { - if (event.value && event.value->IsBoolean()) { - m_controllableValue = event.value->GetBoolean(); - } - } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_p.ReadQueue()) { + m_pData.SetValue(v.value, v.time); + } + for (auto&& v : m_i.ReadQueue()) { + m_iData.SetValue(v.value, v.time); + } + for (auto&& v : m_d.ReadQueue()) { + m_dData.SetValue(v.value, v.time); + } + for (auto&& v : m_setpoint.ReadQueue()) { + m_setpointData.SetValue(v.value, v.time); + } + for (auto&& v : m_controllable.ReadQueue()) { + m_controllableValue = v.value; } } bool NTPIDControllerModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_setpoint) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_setpoint.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTSpeedController.cpp b/glass/src/libnt/native/cpp/NTSpeedController.cpp index 3dc351a188..376160febb 100644 --- a/glass/src/libnt/native/cpp/NTSpeedController.cpp +++ b/glass/src/libnt/native/cpp/NTSpeedController.cpp @@ -10,43 +10,35 @@ using namespace glass; NTSpeedControllerModel::NTSpeedControllerModel(std::string_view path) - : NTSpeedControllerModel(nt::GetDefaultInstance(), path) {} + : NTSpeedControllerModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTSpeedControllerModel::NTSpeedControllerModel(NT_Inst instance, +NTSpeedControllerModel::NTSpeedControllerModel(nt::NetworkTableInstance inst, std::string_view path) - : m_nt(instance), - m_value(m_nt.GetEntry(fmt::format("{}/Value", path))), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_controllable(m_nt.GetEntry(fmt::format("{}/.controllable", path))), - m_valueData(fmt::format("NT_SpdCtrl:{}", path)), - m_nameValue(wpi::rsplit(path, '/').second) { - m_nt.AddListener(m_value); - m_nt.AddListener(m_name); - m_nt.AddListener(m_controllable); -} + : m_inst{inst}, + m_value{inst.GetDoubleTopic(fmt::format("{}/Value", path)) + .GetEntry(0, {{nt::PubSubOption::SendAll(true)}})}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_controllable{inst.GetBooleanTopic(fmt::format("{}/.controllable", path)) + .Subscribe(false)}, + m_valueData{fmt::format("NT_SpdCtrl:{}", path)}, + m_nameValue{wpi::rsplit(path, '/').second} {} void NTSpeedControllerModel::SetPercent(double value) { - nt::SetEntryValue(m_value, nt::NetworkTableValue::MakeDouble(value)); + m_value.Set(value); } void NTSpeedControllerModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_value) { - if (event.value && event.value->IsDouble()) { - m_valueData.SetValue(event.value->GetDouble()); - } - } else if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } else if (event.entry == m_controllable) { - if (event.value && event.value->IsBoolean()) { - m_controllableValue = event.value->GetBoolean(); - } - } + for (auto&& v : m_value.ReadQueue()) { + m_valueData.SetValue(v.value, v.time); + } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_controllable.ReadQueue()) { + m_controllableValue = v.value; } } bool NTSpeedControllerModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_value.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTStringChooser.cpp b/glass/src/libnt/native/cpp/NTStringChooser.cpp index e6a97fae93..39fb98ff55 100644 --- a/glass/src/libnt/native/cpp/NTStringChooser.cpp +++ b/glass/src/libnt/native/cpp/NTStringChooser.cpp @@ -9,67 +9,56 @@ using namespace glass; NTStringChooserModel::NTStringChooserModel(std::string_view path) - : NTStringChooserModel{nt::GetDefaultInstance(), path} {} + : NTStringChooserModel{nt::NetworkTableInstance::GetDefault(), path} {} -NTStringChooserModel::NTStringChooserModel(NT_Inst inst, std::string_view path) - : m_nt{inst}, - m_default{m_nt.GetEntry(fmt::format("{}/default", path))}, - m_selected{m_nt.GetEntry(fmt::format("{}/selected", path))}, - m_active{m_nt.GetEntry(fmt::format("{}/active", path))}, - m_options{m_nt.GetEntry(fmt::format("{}/options", path))} { - m_nt.AddListener(m_default); - m_nt.AddListener(m_selected); - m_nt.AddListener(m_active); - m_nt.AddListener(m_options); -} - -void NTStringChooserModel::SetDefault(std::string_view val) { - nt::SetEntryValue(m_default, nt::Value::MakeString(val)); +NTStringChooserModel::NTStringChooserModel(nt::NetworkTableInstance inst, + std::string_view path) + : m_inst{inst}, + m_default{ + m_inst.GetStringTopic(fmt::format("{}/default", path)).Subscribe("")}, + m_selected{ + m_inst.GetStringTopic(fmt::format("{}/selected", path)).GetEntry("")}, + m_active{ + m_inst.GetStringTopic(fmt::format("{}/active", path)).Subscribe("")}, + m_options{m_inst.GetStringArrayTopic(fmt::format("{}/options", path)) + .Subscribe({})} { + m_selected.GetTopic().SetRetained(true); } void NTStringChooserModel::SetSelected(std::string_view val) { - nt::SetEntryValue(m_selected, nt::Value::MakeString(val)); -} - -void NTStringChooserModel::SetActive(std::string_view val) { - nt::SetEntryValue(m_active, nt::Value::MakeString(val)); -} - -void NTStringChooserModel::SetOptions(wpi::span val) { - nt::SetEntryValue(m_options, nt::Value::MakeStringArray(val)); + m_selected.Set(val); } void NTStringChooserModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_default) { - if ((event.flags & NT_NOTIFY_DELETE) != 0) { - m_defaultValue.clear(); - } else if (event.value && event.value->IsString()) { - m_defaultValue = event.value->GetString(); - } - } else if (event.entry == m_selected) { - if ((event.flags & NT_NOTIFY_DELETE) != 0) { - m_selectedValue.clear(); - } else if (event.value && event.value->IsString()) { - m_selectedValue = event.value->GetString(); - } - } else if (event.entry == m_active) { - if ((event.flags & NT_NOTIFY_DELETE) != 0) { - m_activeValue.clear(); - } else if (event.value && event.value->IsString()) { - m_activeValue = event.value->GetString(); - } - } else if (event.entry == m_options) { - if ((event.flags & NT_NOTIFY_DELETE) != 0) { - m_optionsValue.clear(); - } else if (event.value && event.value->IsStringArray()) { - auto arr = event.value->GetStringArray(); - m_optionsValue.assign(arr.begin(), arr.end()); - } - } + if (!m_default.Exists()) { + m_defaultValue.clear(); + } + for (auto&& v : m_default.ReadQueue()) { + m_defaultValue = std::move(v.value); + } + + if (!m_selected.Exists()) { + m_selectedValue.clear(); + } + for (auto&& v : m_selected.ReadQueue()) { + m_selectedValue = std::move(v.value); + } + + if (!m_active.Exists()) { + m_activeValue.clear(); + } + for (auto&& v : m_active.ReadQueue()) { + m_activeValue = std::move(v.value); + } + + if (!m_options.Exists()) { + m_optionsValue.clear(); + } + for (auto&& v : m_options.ReadQueue()) { + m_optionsValue = std::move(v.value); } } bool NTStringChooserModel::Exists() { - return m_nt.IsConnected() && nt::GetEntryType(m_options) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_options.Exists(); } diff --git a/glass/src/libnt/native/cpp/NTSubsystem.cpp b/glass/src/libnt/native/cpp/NTSubsystem.cpp index b2bdf8c837..093790b598 100644 --- a/glass/src/libnt/native/cpp/NTSubsystem.cpp +++ b/glass/src/libnt/native/cpp/NTSubsystem.cpp @@ -9,37 +9,30 @@ using namespace glass; NTSubsystemModel::NTSubsystemModel(std::string_view path) - : NTSubsystemModel(nt::GetDefaultInstance(), path) {} + : NTSubsystemModel(nt::NetworkTableInstance::GetDefault(), path) {} -NTSubsystemModel::NTSubsystemModel(NT_Inst instance, std::string_view path) - : m_nt(instance), - m_name(m_nt.GetEntry(fmt::format("{}/.name", path))), - m_defaultCommand(m_nt.GetEntry(fmt::format("{}/.default", path))), - m_currentCommand(m_nt.GetEntry(fmt::format("{}/.command", path))) { - m_nt.AddListener(m_name); - m_nt.AddListener(m_defaultCommand); - m_nt.AddListener(m_currentCommand); +NTSubsystemModel::NTSubsystemModel(nt::NetworkTableInstance inst, + std::string_view path) + : m_inst{inst}, + m_name{inst.GetStringTopic(fmt::format("{}/.name", path)).Subscribe("")}, + m_defaultCommand{ + inst.GetStringTopic(fmt::format("{}/.default", path)).Subscribe("")}, + m_currentCommand{ + inst.GetStringTopic(fmt::format("{}/.command", path)).Subscribe("")} { } void NTSubsystemModel::Update() { - for (auto&& event : m_nt.PollListener()) { - if (event.entry == m_name) { - if (event.value && event.value->IsString()) { - m_nameValue = event.value->GetString(); - } - } else if (event.entry == m_defaultCommand) { - if (event.value && event.value->IsString()) { - m_defaultCommandValue = event.value->GetString(); - } - } else if (event.entry == m_currentCommand) { - if (event.value && event.value->IsString()) { - m_currentCommandValue = event.value->GetString(); - } - } + for (auto&& v : m_name.ReadQueue()) { + m_nameValue = std::move(v.value); + } + for (auto&& v : m_defaultCommand.ReadQueue()) { + m_defaultCommandValue = std::move(v.value); + } + for (auto&& v : m_currentCommand.ReadQueue()) { + m_currentCommandValue = std::move(v.value); } } bool NTSubsystemModel::Exists() { - return m_nt.IsConnected() && - nt::GetEntryType(m_defaultCommand) != NT_UNASSIGNED; + return m_inst.IsConnected() && m_defaultCommand.Exists(); } diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp index 90a9cc67ca..a8e7d6db50 100644 --- a/glass/src/libnt/native/cpp/NetworkTables.cpp +++ b/glass/src/libnt/native/cpp/NetworkTables.cpp @@ -4,8 +4,6 @@ #include "glass/networktables/NetworkTables.h" -#include - #include #include #include @@ -16,10 +14,16 @@ #include #include +#include +#include +#include #include +#include +#include #include #include #include +#include #include #include @@ -28,6 +32,31 @@ #include "glass/Storage.h" using namespace glass; +using namespace mpack; + +namespace { +enum ShowCategory { + ShowPersistent, + ShowRetained, + ShowTransitory, + ShowAll, +}; +} // namespace + +static bool IsVisible(ShowCategory category, bool persistent, bool retained) { + switch (category) { + case ShowPersistent: + return persistent; + case ShowRetained: + return retained && !persistent; + case ShowTransitory: + return !retained && !persistent; + case ShowAll: + return true; + default: + return false; + } +} static std::string BooleanArrayToString(wpi::span in) { std::string rv; @@ -49,7 +78,13 @@ static std::string BooleanArrayToString(wpi::span in) { return rv; } -static std::string DoubleArrayToString(wpi::span in) { +static std::string IntegerArrayToString(wpi::span in) { + return fmt::format("[{:d}]", fmt::join(in, ",")); +} + +template +static std::string FloatArrayToString(wpi::span in) { + static_assert(std::is_same_v || std::is_same_v); return fmt::format("[{:.6f}]", fmt::join(in, ",")); } @@ -72,91 +107,380 @@ static std::string StringArrayToString(wpi::span in) { } NetworkTablesModel::NetworkTablesModel() - : NetworkTablesModel{nt::GetDefaultInstance()} {} + : NetworkTablesModel{nt::NetworkTableInstance::GetDefault()} {} -NetworkTablesModel::NetworkTablesModel(NT_Inst inst) - : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} { - nt::AddPolledEntryListener(m_poller, "", - NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | - NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE | - NT_NOTIFY_FLAGS | NT_NOTIFY_IMMEDIATE); +NetworkTablesModel::NetworkTablesModel(nt::NetworkTableInstance inst) + : m_inst{inst}, + m_subscriber{nt::SubscribeMultiple(inst.GetHandle(), {{"", "$"}})}, + m_topicPoller{inst}, + m_valuePoller{inst} { + m_topicPoller.Add({{""}}, + NT_TOPIC_NOTIFY_IMMEDIATE | NT_TOPIC_NOTIFY_PROPERTIES | + NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_UNPUBLISH); + m_valuePoller.Add(m_subscriber, + NT_VALUE_NOTIFY_IMMEDIATE | NT_VALUE_NOTIFY_LOCAL); } -NetworkTablesModel::~NetworkTablesModel() { - nt::DestroyEntryListenerPoller(m_poller); +NetworkTablesModel::Entry::~Entry() { + if (publisher != 0) { + nt::Unpublish(publisher); + } } -NetworkTablesModel::Entry::Entry(nt::EntryNotification&& event) - : entry{event.entry}, - name{std::move(event.name)}, - value{std::move(event.value)}, - flags{nt::GetEntryFlags(event.entry)} { - UpdateValue(); +void NetworkTablesModel::Entry::UpdateInfo(nt::TopicInfo&& info_) { + info = std::move(info_); + properties = info.GetProperties(); + + persistent = false; + auto it = properties.find("persistent"); + if (it != properties.end()) { + if (auto v = it->get_ptr()) { + persistent = *v; + } + } + + retained = false; + it = properties.find("retained"); + if (it != properties.end()) { + if (auto v = it->get_ptr()) { + retained = *v; + } + } } -void NetworkTablesModel::Entry::UpdateValue() { - switch (value->type()) { +static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out, + mpack_reader_t& r, std::string_view name, + int64_t time) { + mpack_tag_t tag = mpack_read_tag(&r); + switch (mpack_tag_type(&tag)) { + case mpack::mpack_type_bool: + out->UpdateFromValue( + nt::Value::MakeBoolean(mpack_tag_bool_value(&tag), time), name, ""); + break; + case mpack::mpack_type_int: + out->UpdateFromValue( + nt::Value::MakeInteger(mpack_tag_int_value(&tag), time), name, ""); + break; + case mpack::mpack_type_uint: + out->UpdateFromValue( + nt::Value::MakeInteger(mpack_tag_uint_value(&tag), time), name, ""); + break; + case mpack::mpack_type_float: + out->UpdateFromValue( + nt::Value::MakeFloat(mpack_tag_float_value(&tag), time), name, ""); + break; + case mpack::mpack_type_double: + out->UpdateFromValue( + nt::Value::MakeDouble(mpack_tag_double_value(&tag), time), name, ""); + break; + case mpack::mpack_type_str: { + std::string str; + mpack_read_str(&r, &tag, &str); + mpack_done_str(&r); + out->UpdateFromValue(nt::Value::MakeString(std::move(str), time), name, + ""); + break; + } + case mpack::mpack_type_bin: + // just skip it + mpack_skip_bytes(&r, mpack_tag_bin_length(&tag)); + mpack_done_bin(&r); + break; + case mpack::mpack_type_array: { + if (out->valueChildrenMap) { + out->valueChildren.clear(); + out->valueChildrenMap = false; + } + out->valueChildren.resize(mpack_tag_array_count(&tag)); + unsigned int i = 0; + for (auto&& child : out->valueChildren) { + if (child.name.empty()) { + child.name = fmt::format("[{}]", i); + child.path = fmt::format("{}{}", name, child.name); + } + ++i; + UpdateMsgpackValueSource(&child, r, child.path, time); // recurse + } + mpack_done_array(&r); + break; + } + case mpack::mpack_type_map: { + if (!out->valueChildrenMap) { + out->valueChildren.clear(); + out->valueChildrenMap = true; + } + wpi::StringMap elems; + for (size_t i = 0, size = out->valueChildren.size(); i < size; ++i) { + elems[out->valueChildren[i].name] = i; + } + bool added = false; + uint32_t count = mpack_tag_map_count(&tag); + for (uint32_t i = 0; i < count; ++i) { + std::string key; + if (mpack_expect_str(&r, &key) == mpack_ok) { + auto it = elems.find(key); + if (it != elems.end()) { + auto& child = out->valueChildren[it->second]; + UpdateMsgpackValueSource(&child, r, child.path, time); + elems.erase(it); + } else { + added = true; + out->valueChildren.emplace_back(); + auto& child = out->valueChildren.back(); + child.name = std::move(key); + child.path = fmt::format("{}/{}", name, child.name); + UpdateMsgpackValueSource(&child, r, child.path, time); + } + } + } + // erase unmatched keys + out->valueChildren.erase( + std::remove_if( + out->valueChildren.begin(), out->valueChildren.end(), + [&](const auto& child) { return elems.count(child.name) > 0; }), + out->valueChildren.end()); + if (added) { + // sort by name + std::sort(out->valueChildren.begin(), out->valueChildren.end(), + [](const auto& a, const auto& b) { return a.name < b.name; }); + } + mpack_done_map(&r); + break; + } + default: + out->value = {}; + mpack_done_type(&r, mpack_tag_type(&tag)); + break; + } +} + +static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out, + const wpi::json& j, std::string_view name, + int64_t time) { + switch (j.type()) { + case wpi::json::value_t::object: { + if (!out->valueChildrenMap) { + out->valueChildren.clear(); + out->valueChildrenMap = true; + } + wpi::StringMap elems; + for (size_t i = 0, size = out->valueChildren.size(); i < size; ++i) { + elems[out->valueChildren[i].name] = i; + } + bool added = false; + for (auto&& kv : j.items()) { + auto it = elems.find(kv.key()); + if (it != elems.end()) { + auto& child = out->valueChildren[it->second]; + UpdateJsonValueSource(&child, kv.value(), child.path, time); + elems.erase(it); + } else { + added = true; + out->valueChildren.emplace_back(); + auto& child = out->valueChildren.back(); + child.name = kv.key(); + child.path = fmt::format("{}/{}", name, child.name); + UpdateJsonValueSource(&child, kv.value(), child.path, time); + } + } + // erase unmatched keys + out->valueChildren.erase( + std::remove_if( + out->valueChildren.begin(), out->valueChildren.end(), + [&](const auto& child) { return elems.count(child.name) > 0; }), + out->valueChildren.end()); + if (added) { + // sort by name + std::sort(out->valueChildren.begin(), out->valueChildren.end(), + [](const auto& a, const auto& b) { return a.name < b.name; }); + } + break; + } + case wpi::json::value_t::array: { + if (out->valueChildrenMap) { + out->valueChildren.clear(); + out->valueChildrenMap = false; + } + out->valueChildren.resize(j.size()); + unsigned int i = 0; + for (auto&& child : out->valueChildren) { + if (child.name.empty()) { + child.name = fmt::format("[{}]", i); + child.path = fmt::format("{}{}", name, child.name); + } + ++i; + UpdateJsonValueSource(&child, j[i], child.path, time); // recurse + } + break; + } + case wpi::json::value_t::string: + out->UpdateFromValue( + nt::Value::MakeString(j.get_ref(), time), name, + ""); + break; + case wpi::json::value_t::boolean: + out->UpdateFromValue(nt::Value::MakeBoolean(j.get(), time), name, + ""); + break; + case wpi::json::value_t::number_integer: + out->UpdateFromValue(nt::Value::MakeInteger(j.get(), time), name, + ""); + break; + case wpi::json::value_t::number_unsigned: + out->UpdateFromValue(nt::Value::MakeInteger(j.get(), time), + name, ""); + break; + case wpi::json::value_t::number_float: + out->UpdateFromValue(nt::Value::MakeDouble(j.get(), time), name, + ""); + break; + default: + out->value = {}; + break; + } +} + +void NetworkTablesModel::ValueSource::UpdateFromValue( + nt::Value&& v, std::string_view name, std::string_view typeStr) { + value = v; + switch (value.type()) { case NT_BOOLEAN: + valueChildren.clear(); if (!source) { source = std::make_unique(fmt::format("NT:{}", name)); } - source->SetValue(value->GetBoolean() ? 1 : 0); + source->SetValue(value.GetBoolean() ? 1 : 0, value.last_change()); source->SetDigital(true); break; - case NT_DOUBLE: + case NT_INTEGER: + valueChildren.clear(); if (!source) { source = std::make_unique(fmt::format("NT:{}", name)); } - source->SetValue(value->GetDouble()); + source->SetValue(value.GetInteger(), value.last_change()); + source->SetDigital(false); + break; + case NT_FLOAT: + valueChildren.clear(); + if (!source) { + source = std::make_unique(fmt::format("NT:{}", name)); + } + source->SetValue(value.GetFloat(), value.last_change()); + source->SetDigital(false); + break; + case NT_DOUBLE: + valueChildren.clear(); + if (!source) { + source = std::make_unique(fmt::format("NT:{}", name)); + } + source->SetValue(value.GetDouble(), value.last_change()); source->SetDigital(false); break; case NT_BOOLEAN_ARRAY: - valueStr = BooleanArrayToString(value->GetBooleanArray()); + valueChildren.clear(); + valueStr = BooleanArrayToString(value.GetBooleanArray()); + break; + case NT_INTEGER_ARRAY: + valueChildren.clear(); + valueStr = IntegerArrayToString(value.GetIntegerArray()); + break; + case NT_FLOAT_ARRAY: + valueChildren.clear(); + valueStr = FloatArrayToString(value.GetFloatArray()); break; case NT_DOUBLE_ARRAY: - valueStr = DoubleArrayToString(value->GetDoubleArray()); + valueChildren.clear(); + valueStr = FloatArrayToString(value.GetDoubleArray()); break; case NT_STRING_ARRAY: - valueStr = StringArrayToString(value->GetStringArray()); + valueChildren.clear(); + valueStr = StringArrayToString(value.GetStringArray()); + break; + case NT_STRING: + if (typeStr == "json") { + try { + UpdateJsonValueSource(this, wpi::json::parse(value.GetString()), name, + value.last_change()); + } catch (wpi::json::exception&) { + // ignore + } + } else { + valueChildren.clear(); + } + break; + case NT_RAW: + if (typeStr == "msgpack") { + mpack_reader_t r; + mpack_reader_init_data(&r, value.GetRaw()); + UpdateMsgpackValueSource(this, r, name, value.last_change()); + + mpack_reader_destroy(&r); + } else { + valueChildren.clear(); + } break; default: + valueChildren.clear(); break; } } void NetworkTablesModel::Update() { - bool timedOut = false; bool updateTree = false; - for (auto&& event : nt::PollEntryListener(m_poller, 0, &timedOut)) { - auto& entry = m_entries[event.entry]; - if (event.flags & NT_NOTIFY_NEW) { + for (auto&& event : m_topicPoller.ReadQueue()) { + auto& entry = m_entries[event.info.topic]; + if (event.flags & NT_TOPIC_NOTIFY_PUBLISH) { if (!entry) { - entry = std::make_unique(std::move(event)); + entry = std::make_unique(); m_sortedEntries.emplace_back(entry.get()); updateTree = true; - continue; } } - if (!entry) { - continue; - } - if (event.flags & NT_NOTIFY_DELETE) { + if (event.flags & NT_TOPIC_NOTIFY_UNPUBLISH) { auto it = std::find(m_sortedEntries.begin(), m_sortedEntries.end(), entry.get()); // will be removed completely below if (it != m_sortedEntries.end()) { *it = nullptr; } - m_entries.erase(event.entry); + m_entries.erase(event.info.topic); updateTree = true; continue; } - if (event.flags & NT_NOTIFY_UPDATE) { - entry->value = std::move(event.value); - entry->UpdateValue(); + if (event.flags & NT_TOPIC_NOTIFY_PROPERTIES) { + updateTree = true; } - if (event.flags & NT_NOTIFY_FLAGS) { - entry->flags = nt::GetEntryFlags(event.entry); + if (entry) { + entry->UpdateTopic(std::move(event)); + } + } + for (auto&& event : m_valuePoller.ReadQueue()) { + auto& entry = m_entries[event.topic]; + if (entry) { + entry->UpdateFromValue(std::move(event.value), entry->info.name, + entry->info.type_str); + if (wpi::starts_with(entry->info.name, '$') && entry->value.IsRaw() && + entry->info.type_str == "msgpack") { + fmt::print(stderr, "Updating meta-topic {}\n", entry->info.name); + // meta topic handling + if (entry->info.name == "$clients") { + UpdateClients(entry->value.GetRaw()); + } else if (entry->info.name == "$serverpub") { + m_server.UpdatePublishers(entry->value.GetRaw()); + } else if (entry->info.name == "$serversub") { + m_server.UpdateSubscribers(entry->value.GetRaw()); + } else if (wpi::starts_with(entry->info.name, "$clientpub$")) { + auto it = m_clients.find(wpi::drop_front(entry->info.name, 11)); + if (it != m_clients.end()) { + it->second.UpdatePublishers(entry->value.GetRaw()); + } + } else if (wpi::starts_with(entry->info.name, "$clientsub$")) { + auto it = m_clients.find(wpi::drop_front(entry->info.name, 11)); + if (it != m_clients.end()) { + it->second.UpdateSubscribers(entry->value.GetRaw()); + } + } + } } } @@ -170,16 +494,32 @@ void NetworkTablesModel::Update() { std::remove(m_sortedEntries.begin(), m_sortedEntries.end(), nullptr), m_sortedEntries.end()); - // sort by name - std::sort(m_sortedEntries.begin(), m_sortedEntries.end(), - [](const auto& a, const auto& b) { return a->name < b->name; }); + RebuildTree(); +} - // rebuild tree - m_root.clear(); +void NetworkTablesModel::RebuildTree() { + // sort by name + std::sort( + m_sortedEntries.begin(), m_sortedEntries.end(), + [](const auto& a, const auto& b) { return a->info.name < b->info.name; }); + + RebuildTreeImpl(&m_root, ShowAll); + RebuildTreeImpl(&m_persistentRoot, ShowPersistent); + RebuildTreeImpl(&m_retainedRoot, ShowRetained); + RebuildTreeImpl(&m_transitoryRoot, ShowTransitory); +} + +void NetworkTablesModel::RebuildTreeImpl(std::vector* tree, + int category) { + tree->clear(); wpi::SmallVector parts; for (auto& entry : m_sortedEntries) { + if (!IsVisible(static_cast(category), entry->persistent, + entry->retained)) { + continue; + } parts.clear(); - wpi::split(entry->name, parts, '/', -1, false); + wpi::split(entry->info.name, parts, '/', -1, false); // ignore a raw "/" key if (parts.empty()) { @@ -187,7 +527,7 @@ void NetworkTablesModel::Update() { } // get to leaf - auto nodes = &m_root; + auto nodes = tree; for (auto part : wpi::drop_back(wpi::span{parts.begin(), parts.end()})) { auto it = std::find_if(nodes->begin(), nodes->end(), @@ -196,9 +536,10 @@ void NetworkTablesModel::Update() { nodes->emplace_back(part); // path is from the beginning of the string to the end of the current // part; this works because part is a reference to the internals of - // entry->name + // entry->info.name nodes->back().path.assign( - entry->name.data(), part.data() + part.size() - entry->name.data()); + entry->info.name.data(), + part.data() + part.size() - entry->info.name.data()); it = nodes->end() - 1; } nodes = &it->children; @@ -217,14 +558,193 @@ void NetworkTablesModel::Update() { } bool NetworkTablesModel::Exists() { - return nt::IsConnected(m_inst); + return m_inst.IsConnected(); } -static std::shared_ptr StringToBooleanArray(std::string_view in) { +NetworkTablesModel::Entry* NetworkTablesModel::GetEntry(std::string_view name) { + auto entryIt = std::lower_bound( + m_sortedEntries.begin(), m_sortedEntries.end(), name, + [](auto&& entry, auto&& name) { return entry->info.name < name; }); + if (entryIt == m_sortedEntries.end() || (*entryIt)->info.name != name) { + return nullptr; + } + return *entryIt; +} + +NetworkTablesModel::Entry* NetworkTablesModel::AddEntry(NT_Topic topic) { + auto& entry = m_entries[topic]; + if (!entry) { + entry = std::make_unique(); + entry->info = nt::GetTopicInfo(topic); + entry->properties = entry->info.GetProperties(); + m_sortedEntries.emplace_back(entry.get()); + } + RebuildTree(); + return entry.get(); +} + +void NetworkTablesModel::Client::UpdatePublishers( + wpi::span data) { + mpack_reader_t r; + mpack_reader_init_data(&r, data); + uint32_t numPub = mpack_expect_array_max(&r, 1000); + std::vector newPublishers; + newPublishers.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); + newPublishers.emplace_back(std::move(pub)); + } + mpack_done_array(&r); + if (mpack_reader_destroy(&r) == mpack_ok) { + publishers = std::move(newPublishers); + } else { + fmt::print(stderr, "Failed to update publishers\n"); + } +} + +static void DecodeSubscriberOptions( + mpack_reader_t& r, NetworkTablesModel::SubscriberOptions* options) { + *options = NetworkTablesModel::SubscriberOptions{}; + 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 == "immediate") { + options->immediate = mpack_expect_bool(&r); + } else if (key == "sendAll") { + 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); +} + +void NetworkTablesModel::Client::UpdateSubscribers( + wpi::span data) { + mpack_reader_t r; + mpack_reader_init_data(&r, data); + uint32_t numSub = mpack_expect_array_max(&r, 1000); + std::vector newSubscribers; + newSubscribers.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)); + } + } + sub.topicsStr = StringArrayToString(sub.topics); + mpack_done_array(&r); + } else if (key == "options") { + DecodeSubscriberOptions(r, &sub.options); + } else { + mpack_discard(&r); + } + } + mpack_done_map(&r); + newSubscribers.emplace_back(std::move(sub)); + } + mpack_done_array(&r); + if (mpack_reader_destroy(&r) == mpack_ok) { + subscribers = std::move(newSubscribers); + } else { + fmt::print(stderr, "Failed to update subscribers\n"); + } +} + +void NetworkTablesModel::UpdateClients(wpi::span data) { + mpack_reader_t r; + mpack_reader_init_data(&r, data); + uint32_t numClients = mpack_expect_array_max(&r, 100); + std::vector clientsArr; + clientsArr.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") { + uint16_t val = mpack_expect_u16(&r); + client.version = fmt::format("{}.{}", val >> 8, val & 0xff); + } else { + mpack_discard(&r); + } + } + mpack_done_map(&r); + clientsArr.emplace_back(std::move(client)); + } + mpack_done_array(&r); + if (mpack_reader_destroy(&r) != mpack_ok) { + return; + } + + // we need to create a new map so deletions are reflected + std::map> newClients; + for (auto&& client : clientsArr) { + auto& newClient = newClients[client.id]; + newClient = std::move(client); + auto it = m_clients.find(newClient.id); + if (it != m_clients.end()) { + // transfer from existing + newClient.publishers = std::move(it->second.publishers); + newClient.subscribers = std::move(it->second.subscribers); + } else { + // initially populate + if (Entry* entry = GetEntry(fmt::format("$clientpub${}", newClient.id))) { + if (entry->value.IsRaw() && entry->info.type_str == "msgpack") { + newClient.UpdatePublishers(entry->value.GetRaw()); + } + } + if (Entry* entry = GetEntry(fmt::format("$clientsub${}", newClient.id))) { + if (entry->value.IsRaw() && entry->info.type_str == "msgpack") { + newClient.UpdateSubscribers(entry->value.GetRaw()); + } + } + } + } + + // replace map + m_clients = std::move(newClients); +} + +static bool StringToBooleanArray(std::string_view in, std::vector* out) { in = wpi::trim(in); if (in.empty()) { - return nt::NetworkTableValue::MakeBooleanArray( - std::initializer_list{}); + return false; } if (in.front() == '[') { in.remove_prefix(1); @@ -235,30 +755,29 @@ static std::shared_ptr StringToBooleanArray(std::string_view in) { in = wpi::trim(in); wpi::SmallVector inSplit; - wpi::SmallVector out; wpi::split(in, inSplit, ',', -1, false); for (auto val : inSplit) { val = wpi::trim(val); if (wpi::equals_lower(val, "true")) { - out.emplace_back(1); + out->emplace_back(1); } else if (wpi::equals_lower(val, "false")) { - out.emplace_back(0); + out->emplace_back(0); } else { fmt::print(stderr, "GUI: NetworkTables: Could not understand value '{}'\n", val); - return nullptr; + return false; } } - return nt::NetworkTableValue::MakeBooleanArray(out); + return true; } -static std::shared_ptr StringToDoubleArray(std::string_view in) { +static bool StringToIntegerArray(std::string_view in, + std::vector* out) { in = wpi::trim(in); if (in.empty()) { - return nt::NetworkTableValue::MakeDoubleArray( - std::initializer_list{}); + return false; } if (in.front() == '[') { in.remove_prefix(1); @@ -269,20 +788,50 @@ static std::shared_ptr StringToDoubleArray(std::string_view in) { in = wpi::trim(in); wpi::SmallVector inSplit; - wpi::SmallVector out; wpi::split(in, inSplit, ',', -1, false); for (auto val : inSplit) { - if (auto num = wpi::parse_float(wpi::trim(val))) { - out.emplace_back(num.value()); + if (auto num = wpi::parse_integer(wpi::trim(val), 0)) { + out->emplace_back(num.value()); } else { fmt::print(stderr, "GUI: NetworkTables: Could not understand value '{}'\n", val); - return nullptr; + return false; } } - return nt::NetworkTableValue::MakeDoubleArray(out); + return true; +} + +template +static bool StringToFloatArray(std::string_view in, std::vector* out) { + static_assert(std::is_same_v || std::is_same_v); + in = wpi::trim(in); + if (in.empty()) { + return false; + } + if (in.front() == '[') { + in.remove_prefix(1); + } + if (in.back() == ']') { + in.remove_suffix(1); + } + in = wpi::trim(in); + + wpi::SmallVector inSplit; + + wpi::split(in, inSplit, ',', -1, false); + for (auto val : inSplit) { + if (auto num = wpi::parse_float(wpi::trim(val))) { + out->emplace_back(num.value()); + } else { + fmt::print(stderr, + "GUI: NetworkTables: Could not understand value '{}'\n", val); + return false; + } + } + + return true; } static int fromxdigit(char ch) { @@ -333,11 +882,11 @@ static std::string_view UnescapeString(std::string_view source, return {buf.data(), buf.size()}; } -static std::shared_ptr StringToStringArray(std::string_view in) { +static bool StringToStringArray(std::string_view in, + std::vector* out) { in = wpi::trim(in); if (in.empty()) { - return nt::NetworkTableValue::MakeStringArray( - std::initializer_list{}); + return false; } if (in.front() == '[') { in.remove_prefix(1); @@ -348,7 +897,6 @@ static std::shared_ptr StringToStringArray(std::string_view in) { in = wpi::trim(in); wpi::SmallVector inSplit; - std::vector out; wpi::SmallString<32> buf; wpi::split(in, inSplit, ',', -1, false); @@ -360,24 +908,32 @@ static std::shared_ptr StringToStringArray(std::string_view in) { if (val.front() != '"' || val.back() != '"') { fmt::print(stderr, "GUI: NetworkTables: Could not understand value '{}'\n", val); - return nullptr; + return false; } - out.emplace_back(UnescapeString(val, buf)); + out->emplace_back(UnescapeString(val, buf)); } - return nt::NetworkTableValue::MakeStringArray(std::move(out)); + return true; } -static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry, +static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry, + const char* typeStr, NetworkTablesFlags flags) { auto& val = entry.value; if (!val) { return; } - switch (val->type()) { + switch (val.type()) { case NT_BOOLEAN: - ImGui::LabelText("boolean", "%s", val->GetBoolean() ? "true" : "false"); + ImGui::LabelText(typeStr ? typeStr : "boolean", "%s", + val.GetBoolean() ? "true" : "false"); + break; + case NT_INTEGER: + ImGui::LabelText(typeStr ? typeStr : "int", "%" PRId64, val.GetInteger()); + break; + case NT_FLOAT: + ImGui::LabelText(typeStr ? typeStr : "double", "%.6f", val.GetFloat()); break; case NT_DOUBLE: { unsigned char precision = (flags & NetworkTablesFlags_Precision) >> @@ -386,8 +942,9 @@ static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry, #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif - ImGui::LabelText("double", fmt::format("%.{}f", precision).c_str(), - val->GetDouble()); + ImGui::LabelText(typeStr ? typeStr : "double", + fmt::format("%.{}f", precision).c_str(), + val.GetDouble()); #ifdef __GNUC__ #pragma GCC diagnostic pop #endif @@ -395,26 +952,35 @@ static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry, } case NT_STRING: { // GetString() comes from a std::string, so it's null terminated - ImGui::LabelText("string", "%s", val->GetString().data()); + ImGui::LabelText(typeStr ? typeStr : "string", "%s", + val.GetString().data()); break; } case NT_BOOLEAN_ARRAY: - ImGui::LabelText("boolean[]", "%s", entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "boolean[]", "%s", + entry.valueStr.c_str()); + break; + case NT_INTEGER_ARRAY: + ImGui::LabelText(typeStr ? typeStr : "int[]", "%s", + entry.valueStr.c_str()); + break; + case NT_FLOAT_ARRAY: + ImGui::LabelText(typeStr ? typeStr : "float[]", "%s", + entry.valueStr.c_str()); break; case NT_DOUBLE_ARRAY: - ImGui::LabelText("double[]", "%s", entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "double[]", "%s", + entry.valueStr.c_str()); break; case NT_STRING_ARRAY: - ImGui::LabelText("string[]", "%s", entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "string[]", "%s", + entry.valueStr.c_str()); break; case NT_RAW: - ImGui::LabelText("raw", "[...]"); - break; - case NT_RPC: - ImGui::LabelText("rpc", "[...]"); + ImGui::LabelText(typeStr ? typeStr : "raw", "[...]"); break; default: - ImGui::LabelText("other", "?"); + ImGui::LabelText(typeStr ? typeStr : "other", "?"); break; } } @@ -436,28 +1002,60 @@ static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry, return; } - ImGui::PushID(entry.name.c_str()); - switch (val->type()) { + const char* typeStr = + entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str(); + ImGui::PushID(entry.info.name.c_str()); + switch (val.type()) { case NT_BOOLEAN: { static const char* boolOptions[] = {"false", "true"}; - int v = val->GetBoolean() ? 1 : 0; - if (ImGui::Combo("boolean", &v, boolOptions, 2)) { - nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeBoolean(v)); + int v = val.GetBoolean() ? 1 : 0; + if (ImGui::Combo(typeStr ? typeStr : "boolean", &v, boolOptions, 2)) { + if (entry.publisher == 0) { + entry.publisher = + nt::Publish(entry.info.topic, NT_BOOLEAN, "boolean"); + } + nt::SetBoolean(entry.publisher, v); + } + break; + } + case NT_INTEGER: { + int64_t v = val.GetInteger(); + if (ImGui::InputScalar(typeStr ? typeStr : "int", ImGuiDataType_S64, &v, + nullptr, nullptr, nullptr, + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (entry.publisher == 0) { + entry.publisher = nt::Publish(entry.info.topic, NT_INTEGER, "int"); + } + nt::SetInteger(entry.publisher, v); + } + break; + } + case NT_FLOAT: { + float v = val.GetFloat(); + if (ImGui::InputFloat(typeStr ? typeStr : "float", &v, 0, 0, "%.6f", + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (entry.publisher == 0) { + entry.publisher = nt::Publish(entry.info.topic, NT_FLOAT, "float"); + } + nt::SetFloat(entry.publisher, v); } break; } case NT_DOUBLE: { - double v = val->GetDouble(); + double v = val.GetDouble(); unsigned char precision = (flags & NetworkTablesFlags_Precision) >> kNetworkTablesFlags_PrecisionBitShift; #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif - if (ImGui::InputDouble("double", &v, 0, 0, + if (ImGui::InputDouble(typeStr ? typeStr : "double", &v, 0, 0, fmt::format("%.{}f", precision).c_str(), ImGuiInputTextFlags_EnterReturnsTrue)) { - nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeDouble(v)); + if (entry.publisher == 0) { + entry.publisher = nt::Publish(entry.info.topic, NT_DOUBLE, "double"); + } + nt::SetDouble(entry.publisher, v); } #ifdef __GNUC__ #pragma GCC diagnostic pop @@ -465,60 +1063,120 @@ static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry, break; } case NT_STRING: { - char* v = GetTextBuffer(val->GetString()); - if (ImGui::InputText("string", v, kTextBufferSize, + char* v = GetTextBuffer(val.GetString()); + if (ImGui::InputText(typeStr ? typeStr : "string", v, kTextBufferSize, ImGuiInputTextFlags_EnterReturnsTrue)) { - nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeString(v)); + if (entry.publisher == 0) { + entry.publisher = nt::Publish(entry.info.topic, NT_STRING, "string"); + } + nt::SetString(entry.publisher, v); } break; } case NT_BOOLEAN_ARRAY: { char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText("boolean[]", v, kTextBufferSize, + if (ImGui::InputText(typeStr ? typeStr : "boolean[]", v, kTextBufferSize, ImGuiInputTextFlags_EnterReturnsTrue)) { - if (auto outv = StringToBooleanArray(v)) { - nt::SetEntryValue(entry.entry, std::move(outv)); + std::vector outv; + if (StringToBooleanArray(v, &outv)) { + if (entry.publisher == 0) { + entry.publisher = + nt::Publish(entry.info.topic, NT_BOOLEAN_ARRAY, "boolean[]"); + } + nt::SetBooleanArray(entry.publisher, outv); + } + } + break; + } + case NT_INTEGER_ARRAY: { + char* v = GetTextBuffer(entry.valueStr); + if (ImGui::InputText(typeStr ? typeStr : "int[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + std::vector outv; + if (StringToIntegerArray(v, &outv)) { + if (entry.publisher == 0) { + entry.publisher = + nt::Publish(entry.info.topic, NT_INTEGER_ARRAY, "int[]"); + } + nt::SetIntegerArray(entry.publisher, outv); + } + } + break; + } + case NT_FLOAT_ARRAY: { + char* v = GetTextBuffer(entry.valueStr); + if (ImGui::InputText(typeStr ? typeStr : "float[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + std::vector outv; + if (StringToFloatArray(v, &outv)) { + if (entry.publisher == 0) { + entry.publisher = + nt::Publish(entry.info.topic, NT_DOUBLE_ARRAY, "float[]"); + } + nt::SetFloatArray(entry.publisher, outv); } } break; } case NT_DOUBLE_ARRAY: { char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText("double[]", v, kTextBufferSize, + if (ImGui::InputText(typeStr ? typeStr : "double[]", v, kTextBufferSize, ImGuiInputTextFlags_EnterReturnsTrue)) { - if (auto outv = StringToDoubleArray(v)) { - nt::SetEntryValue(entry.entry, std::move(outv)); + std::vector outv; + if (StringToFloatArray(v, &outv)) { + if (entry.publisher == 0) { + entry.publisher = + nt::Publish(entry.info.topic, NT_DOUBLE_ARRAY, "double[]"); + } + nt::SetDoubleArray(entry.publisher, outv); } } break; } case NT_STRING_ARRAY: { char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText("string[]", v, kTextBufferSize, + if (ImGui::InputText(typeStr ? typeStr : "string[]", v, kTextBufferSize, ImGuiInputTextFlags_EnterReturnsTrue)) { - if (auto outv = StringToStringArray(v)) { - nt::SetEntryValue(entry.entry, std::move(outv)); + std::vector outv; + if (StringToStringArray(v, &outv)) { + if (entry.publisher == 0) { + entry.publisher = + nt::Publish(entry.info.topic, NT_STRING_ARRAY, "string[]"); + } + nt::SetStringArray(entry.publisher, outv); } } break; } case NT_RAW: - ImGui::LabelText("raw", "[...]"); + ImGui::LabelText(typeStr ? typeStr : "raw", + val.GetRaw().empty() ? "[]" : "[...]"); break; case NT_RPC: - ImGui::LabelText("rpc", "[...]"); + ImGui::LabelText(typeStr ? typeStr : "rpc", "[...]"); break; default: - ImGui::LabelText("other", "?"); + ImGui::LabelText(typeStr ? typeStr : "other", "?"); break; } ImGui::PopID(); } -static void EmitParentContextMenu(const std::string& path, +static void CreateTopicMenuItem(NetworkTablesModel* model, + std::string_view path, NT_Type type, + const char* typeStr, bool enabled) { + if (ImGui::MenuItem(typeStr, nullptr, false, enabled)) { + auto entry = + model->AddEntry(nt::GetTopic(model->GetInstance().GetHandle(), path)); + if (entry->publisher == 0) { + entry->publisher = nt::Publish(entry->info.topic, type, typeStr); + } + } +} + +static void EmitParentContextMenu(NetworkTablesModel* model, + const std::string& path, NetworkTablesFlags flags) { - // Workaround https://github.com/ocornut/imgui/issues/331 - bool openWarningPopup = false; static char nameBuffer[kTextBufferSize]; if (ImGui::BeginPopupContextItem(path.c_str())) { ImGui::Text("%s", path.c_str()); @@ -540,216 +1198,367 @@ static void EmitParentContextMenu(const std::string& path, ImGui::Text("Adding: %s", fullNewPath.c_str()); ImGui::Separator(); - auto entry = nt::GetEntry(nt::GetDefaultInstance(), fullNewPath); + auto entry = model->GetEntry(fullNewPath); + bool exists = entry && entry->info.type != NT_Type::NT_UNASSIGNED; bool enabled = (flags & NetworkTablesFlags_CreateNoncanonicalKeys || nameBuffer[0] != '\0') && - nt::GetEntryType(entry) == NT_Type::NT_UNASSIGNED; - if (ImGui::MenuItem("string", nullptr, false, enabled)) { - if (!nt::SetEntryValue(entry, nt::Value::MakeString(""))) { - openWarningPopup = true; - } - } - if (ImGui::MenuItem("double", nullptr, false, enabled)) { - if (!nt::SetEntryValue(entry, nt::Value::MakeDouble(0.0))) { - openWarningPopup = true; - } - } - if (ImGui::MenuItem("boolean", nullptr, false, enabled)) { - if (!nt::SetEntryValue(entry, nt::Value::MakeBoolean(false))) { - openWarningPopup = true; - } - } - if (ImGui::MenuItem("string[]", nullptr, false, enabled)) { - if (!nt::SetEntryValue(entry, nt::Value::MakeStringArray({""}))) { - openWarningPopup = true; - } - } - if (ImGui::MenuItem("double[]", nullptr, false, enabled)) { - if (!nt::SetEntryValue(entry, nt::Value::MakeDoubleArray({0.0}))) { - openWarningPopup = true; - } - } - if (ImGui::MenuItem("boolean[]", nullptr, false, enabled)) { - if (!nt::SetEntryValue(entry, nt::Value::MakeBooleanArray({false}))) { - openWarningPopup = true; - } - } + !exists; + + CreateTopicMenuItem(model, fullNewPath, NT_STRING, "string", enabled); + CreateTopicMenuItem(model, fullNewPath, NT_INTEGER, "int", enabled); + CreateTopicMenuItem(model, fullNewPath, NT_FLOAT, "float", enabled); + CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE, "double", enabled); + CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN, "boolean", enabled); + CreateTopicMenuItem(model, fullNewPath, NT_STRING_ARRAY, "string[]", + enabled); + CreateTopicMenuItem(model, fullNewPath, NT_INTEGER_ARRAY, "int[]", + enabled); + CreateTopicMenuItem(model, fullNewPath, NT_FLOAT_ARRAY, "float[]", + enabled); + CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE_ARRAY, "double[]", + enabled); + CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN_ARRAY, "boolean[]", + enabled); ImGui::EndMenu(); } - ImGui::Separator(); - if (ImGui::MenuItem("Remove All")) { - for (auto&& entry : nt::GetEntries(nt::GetDefaultInstance(), path, 0)) { - nt::DeleteEntry(entry); - } - } - ImGui::EndPopup(); - } - - // Workaround https://github.com/ocornut/imgui/issues/331 - if (openWarningPopup) { - ImGui::OpenPopup("Value exists"); - } - if (ImGui::BeginPopupModal("Value exists", nullptr, - ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("The provided name %s already exists in the tree!", nameBuffer); - ImGui::Separator(); - - if (ImGui::Button("OK", ImVec2(120, 0))) { - ImGui::CloseCurrentPopup(); - } - ImGui::SetItemDefaultFocus(); ImGui::EndPopup(); } } -static void EmitEntry(NetworkTablesModel::Entry& entry, const char* name, - NetworkTablesFlags flags) { - if (entry.source) { +static void EmitValueName(DataSource* source, const char* name, + const char* path) { + if (source) { ImGui::Selectable(name); - entry.source->EmitDrag(); + source->EmitDrag(); } else { - ImGui::Text("%s", name); + ImGui::TextUnformatted(name); } - if (ImGui::BeginPopupContextItem(entry.name.c_str())) { - ImGui::Text("%s", entry.name.c_str()); - ImGui::Separator(); - if (ImGui::MenuItem("Remove")) { - nt::DeleteEntry(entry.entry); - } + if (ImGui::BeginPopupContextItem(path)) { + ImGui::TextUnformatted(path); ImGui::EndPopup(); } - ImGui::NextColumn(); +} - if (flags & NetworkTablesFlags_ReadOnly) { - EmitEntryValueReadonly(entry, flags); +static void EmitValueTree( + const std::vector& children, + NetworkTablesFlags flags) { + for (auto&& child : children) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + EmitValueName(child.source.get(), child.name.c_str(), child.path.c_str()); + ImGui::TableNextColumn(); + if (!child.valueChildren.empty()) { + char label[64]; + std::snprintf(label, sizeof(label), + child.valueChildrenMap ? "{...}##v_%s" : "[...]##v_%s", + child.name.c_str()); + if (TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth)) { + EmitValueTree(child.valueChildren, flags); + TreePop(); + } + } else { + EmitEntryValueReadonly(child, nullptr, flags); + } + } +} + +static void EmitEntry(NetworkTablesModel* model, + NetworkTablesModel::Entry& entry, const char* name, + NetworkTablesFlags flags, ShowCategory category) { + if (!IsVisible(category, entry.persistent, entry.retained)) { + return; + } + + bool valueChildrenOpen = false; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + EmitValueName(entry.source.get(), name, entry.info.name.c_str()); + + ImGui::TableNextColumn(); + if (!entry.valueChildren.empty()) { + auto pos = ImGui::GetCursorPos(); + char label[64]; + std::snprintf(label, sizeof(label), + entry.valueChildrenMap ? "{...}##v_%s" : "[...]##v_%s", + entry.info.name.c_str()); + valueChildrenOpen = + TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth | + ImGuiTreeNodeFlags_AllowItemOverlap); + // make it look like a normal label w/type + ImGui::SetCursorPos(pos); + ImGui::LabelText(entry.info.type_str.c_str(), "%s", ""); + } else if (flags & NetworkTablesFlags_ReadOnly) { + EmitEntryValueReadonly( + entry, + entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str(), + flags); } else { EmitEntryValueEditable(entry, flags); } - ImGui::NextColumn(); - if (flags & NetworkTablesFlags_ShowFlags) { - if ((entry.flags & NT_PERSISTENT) != 0) { - ImGui::Text("Persistent"); - } else if (entry.flags != 0) { - ImGui::Text("%02x", entry.flags); + if (flags & NetworkTablesFlags_ShowProperties) { + ImGui::TableNextColumn(); + ImGui::Text("%s", entry.info.properties.c_str()); + if (ImGui::BeginPopupContextItem(entry.info.name.c_str())) { + if (ImGui::Checkbox("persistent", &entry.persistent)) { + nt::SetTopicPersistent(entry.info.topic, entry.persistent); + } + if (ImGui::Checkbox("retained", &entry.retained)) { + if (entry.retained) { + nt::SetTopicProperty(entry.info.topic, "retained", true); + } else { + nt::DeleteTopicProperty(entry.info.topic, "retained"); + } + } + ImGui::EndPopup(); } - ImGui::NextColumn(); } if (flags & NetworkTablesFlags_ShowTimestamp) { + ImGui::TableNextColumn(); if (entry.value) { - ImGui::Text("%f", (entry.value->last_change() * 1.0e-6) - + ImGui::Text("%f", (entry.value.last_change() * 1.0e-6) - (GetZeroTime() * 1.0e-6)); } else { ImGui::TextUnformatted(""); } - ImGui::NextColumn(); } - ImGui::Separator(); + + if (flags & NetworkTablesFlags_ShowServerTimestamp) { + ImGui::TableNextColumn(); + if (entry.value && entry.value.server_time() != 0) { + if (entry.value.server_time() == 1) { + ImGui::TextUnformatted("---"); + } else { + ImGui::Text("%f", entry.value.server_time() * 1.0e-6); + } + } else { + ImGui::TextUnformatted(""); + } + } + + if (valueChildrenOpen) { + EmitValueTree(entry.valueChildren, flags); + TreePop(); + } } -static void EmitTree(const std::vector& tree, - NetworkTablesFlags flags) { +static void EmitTree(NetworkTablesModel* model, + const std::vector& tree, + NetworkTablesFlags flags, ShowCategory category, + bool root) { for (auto&& node : tree) { + if (root && (flags & NetworkTablesFlags_ShowSpecial) == 0 && + wpi::starts_with(node.name, '$')) { + continue; + } if (node.entry) { - EmitEntry(*node.entry, node.name.c_str(), flags); + EmitEntry(model, *node.entry, node.name.c_str(), flags, category); } if (!node.children.empty()) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); bool open = TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth); - EmitParentContextMenu(node.path, flags); - ImGui::NextColumn(); - ImGui::NextColumn(); - if (flags & NetworkTablesFlags_ShowFlags) { - ImGui::NextColumn(); - } - if (flags & NetworkTablesFlags_ShowTimestamp) { - ImGui::NextColumn(); - } - ImGui::Separator(); + EmitParentContextMenu(model, node.path, flags); if (open) { - EmitTree(node.children, flags); + EmitTree(model, node.children, flags, category, false); TreePop(); } } } } -void glass::DisplayNetworkTables(NetworkTablesModel* model, - NetworkTablesFlags flags) { - auto inst = model->GetInstance(); - - if (flags & NetworkTablesFlags_ShowConnections) { - if (CollapsingHeader("Connections")) { - ImGui::Columns(4, "connections"); - ImGui::Text("Id"); - ImGui::NextColumn(); - ImGui::Text("Address"); - ImGui::NextColumn(); - ImGui::Text("Updated"); - ImGui::NextColumn(); - ImGui::Text("Proto"); - ImGui::NextColumn(); - ImGui::Separator(); - for (auto&& i : nt::GetConnections(inst)) { - ImGui::Text("%s", i.remote_id.c_str()); - ImGui::NextColumn(); - ImGui::Text("%s", i.remote_ip.c_str()); - ImGui::NextColumn(); - ImGui::Text("%llu", - static_cast( // NOLINT(runtime/int) - i.last_update)); - ImGui::NextColumn(); - ImGui::Text("%d.%d", i.protocol_version >> 8, - i.protocol_version & 0xff); - ImGui::NextColumn(); - } - ImGui::Columns(); - } - - if (!CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) { - return; - } +static void DisplayTable(NetworkTablesModel* model, + const std::vector& tree, + NetworkTablesFlags flags, ShowCategory category) { + if (tree.empty()) { + return; } - const bool showFlags = (flags & NetworkTablesFlags_ShowFlags); + const bool showProperties = (flags & NetworkTablesFlags_ShowProperties); const bool showTimestamp = (flags & NetworkTablesFlags_ShowTimestamp); + const bool showServerTimestamp = + (flags & NetworkTablesFlags_ShowServerTimestamp); - static bool first = true; - ImGui::Columns(2 + (showFlags ? 1 : 0) + (showTimestamp ? 1 : 0), "values"); - if (first) { - ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth()); - } - ImGui::Text("Name"); - EmitParentContextMenu("/", flags); - ImGui::NextColumn(); - ImGui::Text("Value"); - ImGui::NextColumn(); - if (showFlags) { - if (first) { - ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize()); - } - ImGui::Text("Flags"); - ImGui::NextColumn(); + ImGui::BeginTable("values", + 2 + (showProperties ? 1 : 0) + (showTimestamp ? 1 : 0) + + (showServerTimestamp ? 1 : 0), + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_BordersInner); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, + 0.35f * ImGui::GetWindowWidth()); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, + 12 * ImGui::GetFontSize()); + if (showProperties) { + ImGui::TableSetupColumn("Properties", ImGuiTableColumnFlags_WidthFixed, + 12 * ImGui::GetFontSize()); } if (showTimestamp) { - ImGui::Text("Changed"); - ImGui::NextColumn(); + ImGui::TableSetupColumn("Time"); } - ImGui::Separator(); - first = false; + if (showServerTimestamp) { + ImGui::TableSetupColumn("Server Time"); + } + ImGui::TableHeadersRow(); + // EmitParentContextMenu(model, "/", flags); if (flags & NetworkTablesFlags_TreeView) { - EmitTree(model->GetTreeRoot(), flags); + switch (category) { + case ShowPersistent: + PushID("persistent"); + break; + case ShowRetained: + PushID("retained"); + break; + case ShowTransitory: + PushID("transitory"); + break; + default: + break; + } + EmitTree(model, tree, flags, category, true); + if (category != ShowAll) { + PopID(); + } } else { for (auto entry : model->GetEntries()) { - EmitEntry(*entry, entry->name.c_str(), flags); + if ((flags & NetworkTablesFlags_ShowSpecial) != 0 || + !wpi::starts_with(entry->info.name, '$')) { + EmitEntry(model, *entry, entry->info.name.c_str(), flags, category); + } + } + } + ImGui::EndTable(); +} + +static void DisplayClient(const NetworkTablesModel::Client& client) { + if (CollapsingHeader("Publishers")) { + ImGui::BeginTable("publishers", 2, ImGuiTableFlags_Resizable); + ImGui::TableSetupColumn("UID", ImGuiTableColumnFlags_WidthFixed, + 10 * ImGui::GetFontSize()); + ImGui::TableSetupColumn("Topic"); + ImGui::TableHeadersRow(); + for (auto&& pub : client.publishers) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%" PRId64, pub.uid); + ImGui::TableNextColumn(); + ImGui::Text("%s", pub.topic.c_str()); + } + ImGui::EndTable(); + } + if (CollapsingHeader("Subscribers")) { + ImGui::BeginTable( + "subscribers", 6, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); + ImGui::TableSetupColumn("UID", ImGuiTableColumnFlags_WidthFixed, + 10 * ImGui::GetFontSize()); + ImGui::TableSetupColumn("Topics", ImGuiTableColumnFlags_WidthStretch, 6.0f); + ImGui::TableSetupColumn("Periodic", ImGuiTableColumnFlags_WidthStretch, + 1.0f); + ImGui::TableSetupColumn("Immediate", ImGuiTableColumnFlags_WidthStretch, + 1.0f); + ImGui::TableSetupColumn("Send All", ImGuiTableColumnFlags_WidthStretch, + 1.0f); + ImGui::TableSetupColumn("Prefix Match", ImGuiTableColumnFlags_WidthStretch, + 1.0f); + ImGui::TableHeadersRow(); + for (auto&& sub : client.subscribers) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%" PRId64, sub.uid); + ImGui::TableNextColumn(); + ImGui::Text("%s", sub.topicsStr.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%0.3f", sub.options.periodic); + ImGui::TableNextColumn(); + ImGui::Text(sub.options.immediate ? "Yes" : "No"); + ImGui::TableNextColumn(); + ImGui::Text(sub.options.sendAll ? "Yes" : "No"); + ImGui::TableNextColumn(); + ImGui::Text(sub.options.prefixMatch ? "Yes" : "No"); + } + ImGui::EndTable(); + } +} + +void glass::DisplayNetworkTablesInfo(NetworkTablesModel* model) { + auto inst = model->GetInstance(); + + if (CollapsingHeader("Connections")) { + ImGui::BeginTable("connections", 4, ImGuiTableFlags_Resizable); + ImGui::TableSetupColumn("Id"); + ImGui::TableSetupColumn("Address"); + ImGui::TableSetupColumn("Updated"); + ImGui::TableSetupColumn("Proto"); + ImGui::TableSetupScrollFreeze(1, 0); + ImGui::TableHeadersRow(); + for (auto&& i : inst.GetConnections()) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", i.remote_id.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%s", i.remote_ip.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%llu", + static_cast( // NOLINT(runtime/int) + i.last_update)); + ImGui::TableNextColumn(); + ImGui::Text("%d.%d", i.protocol_version >> 8, i.protocol_version & 0xff); + } + ImGui::EndTable(); + } + + auto netMode = inst.GetNetworkMode(); + if (netMode == NT_NET_MODE_SERVER || netMode == NT_NET_MODE_CLIENT4) { + if (CollapsingHeader("Server")) { + PushID("Server"); + ImGui::Indent(); + DisplayClient(model->GetServer()); + ImGui::Unindent(); + PopID(); + } + if (CollapsingHeader("Clients")) { + ImGui::Indent(); + for (auto&& client : model->GetClients()) { + if (CollapsingHeader(client.second.id.c_str())) { + PushID(client.second.id.c_str()); + ImGui::Indent(); + ImGui::Text("%s (version %s)", client.second.conn.c_str(), + client.second.version.c_str()); + DisplayClient(client.second); + ImGui::Unindent(); + PopID(); + } + } + ImGui::Unindent(); + } + } +} + +void glass::DisplayNetworkTables(NetworkTablesModel* model, + NetworkTablesFlags flags) { + if (flags & NetworkTablesFlags_CombinedView) { + DisplayTable(model, model->GetTreeRoot(), flags, ShowAll); + } else { + if (CollapsingHeader("Persistent Values", ImGuiTreeNodeFlags_DefaultOpen)) { + DisplayTable(model, model->GetPersistentTreeRoot(), flags, + ShowPersistent); + } + + if (CollapsingHeader("Retained Values", ImGuiTreeNodeFlags_DefaultOpen)) { + DisplayTable(model, model->GetRetainedTreeRoot(), flags, ShowRetained); + } + + if (CollapsingHeader("Transitory Values", ImGuiTreeNodeFlags_DefaultOpen)) { + DisplayTable(model, model->GetTransitoryTreeRoot(), flags, + ShowTransitory); } } - ImGui::Columns(); } void NetworkTablesFlagsSettings::Update() { @@ -757,12 +1566,17 @@ void NetworkTablesFlagsSettings::Update() { auto& storage = GetStorage(); m_pTreeView = &storage.GetBool("tree", m_defaultFlags & NetworkTablesFlags_TreeView); - m_pShowConnections = &storage.GetBool( - "connections", m_defaultFlags & NetworkTablesFlags_ShowConnections); - m_pShowFlags = &storage.GetBool( - "flags", m_defaultFlags & NetworkTablesFlags_ShowFlags); + m_pCombinedView = &storage.GetBool( + "combined", m_defaultFlags & NetworkTablesFlags_CombinedView); + m_pShowSpecial = &storage.GetBool( + "special", m_defaultFlags & NetworkTablesFlags_ShowSpecial); + m_pShowProperties = &storage.GetBool( + "properties", m_defaultFlags & NetworkTablesFlags_ShowProperties); m_pShowTimestamp = &storage.GetBool( "timestamp", m_defaultFlags & NetworkTablesFlags_ShowTimestamp); + m_pShowServerTimestamp = &storage.GetBool( + "serverTimestamp", + m_defaultFlags & NetworkTablesFlags_ShowServerTimestamp); m_pCreateNoncanonicalKeys = &storage.GetBool( "createNonCanonical", m_defaultFlags & NetworkTablesFlags_CreateNoncanonicalKeys); @@ -772,14 +1586,18 @@ void NetworkTablesFlagsSettings::Update() { } m_flags &= ~( - NetworkTablesFlags_TreeView | NetworkTablesFlags_ShowConnections | - NetworkTablesFlags_ShowFlags | NetworkTablesFlags_ShowTimestamp | + NetworkTablesFlags_TreeView | NetworkTablesFlags_CombinedView | + NetworkTablesFlags_ShowSpecial | NetworkTablesFlags_ShowProperties | + NetworkTablesFlags_ShowTimestamp | + NetworkTablesFlags_ShowServerTimestamp | NetworkTablesFlags_CreateNoncanonicalKeys | NetworkTablesFlags_Precision); m_flags |= (*m_pTreeView ? NetworkTablesFlags_TreeView : 0) | - (*m_pShowConnections ? NetworkTablesFlags_ShowConnections : 0) | - (*m_pShowFlags ? NetworkTablesFlags_ShowFlags : 0) | + (*m_pCombinedView ? NetworkTablesFlags_CombinedView : 0) | + (*m_pShowSpecial ? NetworkTablesFlags_ShowSpecial : 0) | + (*m_pShowProperties ? NetworkTablesFlags_ShowProperties : 0) | (*m_pShowTimestamp ? NetworkTablesFlags_ShowTimestamp : 0) | + (*m_pShowServerTimestamp ? NetworkTablesFlags_ShowServerTimestamp : 0) | (*m_pCreateNoncanonicalKeys ? NetworkTablesFlags_CreateNoncanonicalKeys : 0) | (*m_pPrecision << kNetworkTablesFlags_PrecisionBitShift); @@ -790,9 +1608,11 @@ void NetworkTablesFlagsSettings::DisplayMenu() { return; } ImGui::MenuItem("Tree View", "", m_pTreeView); - ImGui::MenuItem("Show Connections", "", m_pShowConnections); - ImGui::MenuItem("Show Flags", "", m_pShowFlags); + ImGui::MenuItem("Combined View", "", m_pCombinedView); + ImGui::MenuItem("Show Special", "", m_pShowSpecial); + ImGui::MenuItem("Show Properties", "", m_pShowProperties); ImGui::MenuItem("Show Timestamp", "", m_pShowTimestamp); + ImGui::MenuItem("Show Server Timestamp", "", m_pShowServerTimestamp); if (ImGui::BeginMenu("Decimal Precision")) { static const char* precisionOptions[] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; diff --git a/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp deleted file mode 100644 index 5cb5bbcd38..0000000000 --- a/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp +++ /dev/null @@ -1,19 +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 "glass/networktables/NetworkTablesHelper.h" - -using namespace glass; - -NetworkTablesHelper::NetworkTablesHelper(NT_Inst inst) - : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {} - -NetworkTablesHelper::~NetworkTablesHelper() { - nt::DestroyEntryListenerPoller(m_poller); -} - -bool NetworkTablesHelper::IsConnected() const { - return nt::GetNetworkMode(m_inst) == NT_NET_MODE_SERVER || - nt::IsConnected(m_inst); -} diff --git a/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp index 9ccbb2e904..5120177581 100644 --- a/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp +++ b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp @@ -17,16 +17,19 @@ using namespace glass; NetworkTablesProvider::NetworkTablesProvider(Storage& storage) - : NetworkTablesProvider{storage, nt::GetDefaultInstance()} {} + : NetworkTablesProvider{storage, nt::NetworkTableInstance::GetDefault()} {} -NetworkTablesProvider::NetworkTablesProvider(Storage& storage, NT_Inst inst) +NetworkTablesProvider::NetworkTablesProvider(Storage& storage, + nt::NetworkTableInstance inst) : Provider{storage.GetChild("windows")}, - m_nt{inst}, + m_inst{inst}, + m_topicPoller{inst}, + m_valuePoller{inst}, m_typeCache{storage.GetChild("types")} { storage.SetCustomApply([this] { - m_listener = - m_nt.AddListener("", NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | - NT_NOTIFY_DELETE | NT_NOTIFY_IMMEDIATE); + m_topicListener = m_topicPoller.Add({{""}}, NT_TOPIC_NOTIFY_PUBLISH | + NT_TOPIC_NOTIFY_UNPUBLISH | + NT_TOPIC_NOTIFY_IMMEDIATE); for (auto&& childIt : m_storage.GetChildren()) { auto id = childIt.key(); auto typePtr = m_typeCache.FindValue(id); @@ -41,16 +44,15 @@ NetworkTablesProvider::NetworkTablesProvider(Storage& storage, NT_Inst inst) } auto entry = GetOrCreateView( - builderIt->second, - nt::GetEntry(m_nt.GetInstance(), fmt::format("{}/.type", id)), id); + builderIt->second, m_inst.GetTopic(fmt::format("{}/.type", id)), id); if (entry) { Show(entry, nullptr); } } }); storage.SetCustomClear([this, &storage] { - nt::RemoveEntryListener(m_listener); - m_listener = 0; + m_topicPoller.Remove(m_topicListener); + m_topicListener = 0; for (auto&& modelEntry : m_modelEntries) { modelEntry->model.reset(); } @@ -101,35 +103,57 @@ void NetworkTablesProvider::Update() { Provider::Update(); // add/remove entries from NT changes - for (auto&& event : m_nt.PollListener()) { + for (auto&& event : m_topicPoller.ReadQueue()) { // look for .type fields - std::string_view eventName{event.name}; - if (!wpi::ends_with(eventName, "/.type") || !event.value || - !event.value->IsString()) { + if (!wpi::ends_with(event.info.name, "/.type") || + event.info.type != NT_STRING || event.info.type_str != "string") { + continue; + } + + if (event.flags & NT_TOPIC_NOTIFY_UNPUBLISH) { + auto it = m_topicMap.find(event.info.topic); + if (it != m_topicMap.end()) { + m_valuePoller.Remove(it->second.listener); + m_topicMap.erase(it); + } + + auto it2 = std::find_if( + m_viewEntries.begin(), m_viewEntries.end(), [&](const auto& elem) { + return static_cast(elem->modelEntry) + ->typeTopic.GetHandle() == event.info.topic; + }); + if (it2 != m_viewEntries.end()) { + m_viewEntries.erase(it2); + } + } else if (event.flags & NT_TOPIC_NOTIFY_PUBLISH) { + // subscribe to it + SubListener sublistener; + sublistener.subscriber = nt::StringTopic{event.info.topic}.Subscribe(""); + sublistener.listener = + m_valuePoller.Add(sublistener.subscriber, + NT_VALUE_NOTIFY_LOCAL | NT_VALUE_NOTIFY_IMMEDIATE); + m_topicMap.try_emplace(event.info.topic, std::move(sublistener)); + } + } + + // handle actual .type strings + for (auto&& event : m_valuePoller.ReadQueue()) { + if (!event.value.IsString()) { continue; } - auto tableName = wpi::drop_back(eventName, 6); // only handle ones where we have a builder - auto builderIt = m_typeMap.find(event.value->GetString()); + auto builderIt = m_typeMap.find(event.value.GetString()); if (builderIt == m_typeMap.end()) { continue; } - if (event.flags & NT_NOTIFY_DELETE) { - auto it = std::find_if( - m_viewEntries.begin(), m_viewEntries.end(), [&](const auto& elem) { - return static_cast(elem->modelEntry)->typeEntry == - event.entry; - }); - if (it != m_viewEntries.end()) { - m_viewEntries.erase(it); - } - } else if (event.flags & NT_NOTIFY_NEW) { - GetOrCreateView(builderIt->second, event.entry, tableName); - // cache the type - m_typeCache.SetString(tableName, event.value->GetString()); - } + auto topicName = nt::GetTopicName(event.topic); + auto tableName = wpi::drop_back(topicName, 6); + + GetOrCreateView(builderIt->second, nt::Topic{event.topic}, tableName); + // cache the type + m_typeCache.SetString(tableName, event.value.GetString()); } } @@ -149,7 +173,7 @@ void NetworkTablesProvider::Show(ViewEntry* entry, Window* window) { // get or create model if (!entry->modelEntry->model) { entry->modelEntry->model = - entry->modelEntry->createModel(m_nt.GetInstance(), entry->name.c_str()); + entry->modelEntry->createModel(m_inst, entry->name.c_str()); } if (!entry->modelEntry->model) { return; @@ -180,22 +204,22 @@ void NetworkTablesProvider::Show(ViewEntry* entry, Window* window) { } NetworkTablesProvider::ViewEntry* NetworkTablesProvider::GetOrCreateView( - const Builder& builder, NT_Entry typeEntry, std::string_view name) { + const Builder& builder, nt::Topic typeTopic, std::string_view name) { // get view entry if it already exists auto viewIt = FindViewEntry(name); if (viewIt != m_viewEntries.end() && (*viewIt)->name == name) { // make sure typeEntry is set in model - static_cast((*viewIt)->modelEntry)->typeEntry = typeEntry; + static_cast((*viewIt)->modelEntry)->typeTopic = typeTopic; return viewIt->get(); } // get or create model entry auto modelIt = FindModelEntry(name); if (modelIt != m_modelEntries.end() && (*modelIt)->name == name) { - static_cast(modelIt->get())->typeEntry = typeEntry; + static_cast(modelIt->get())->typeTopic = typeTopic; } else { modelIt = m_modelEntries.emplace( - modelIt, std::make_unique(typeEntry, name, builder)); + modelIt, std::make_unique(typeTopic, name, builder)); } // create new view entry diff --git a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp index d1fb341bcf..40b4e84e3a 100644 --- a/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp +++ b/glass/src/libnt/native/cpp/NetworkTablesSettings.cpp @@ -43,50 +43,63 @@ void NetworkTablesSettings::Thread::Main() { // if just changing servers in client mode, no need to stop and restart unsigned int curMode = nt::GetNetworkMode(m_inst); - if (mode != 1 || (curMode & NT_NET_MODE_SERVER) != 0) { + if ((mode == 0 || mode == 3) || + (mode == 1 && (curMode & NT_NET_MODE_CLIENT4) == 0) || + (mode == 2 && (curMode & NT_NET_MODE_CLIENT3) == 0)) { nt::StopClient(m_inst); nt::StopServer(m_inst); nt::StopLocal(m_inst); } - if (m_mode != 1 || !dsClient) { + if ((m_mode == 0 || m_mode == 3) || !dsClient) { nt::StopDSClient(m_inst); } lock.lock(); } while (mode != m_mode || dsClient != m_dsClient); - if (m_mode == 1) { + if (m_mode == 1 || m_mode == 2) { std::string_view serverTeam{m_serverTeam}; std::optional team; + nt::SetNetworkIdentity(m_inst, m_clientName); + if (m_mode == 1) { + nt::StartClient4(m_inst); + } else if (m_mode == 2) { + nt::StartClient3(m_inst); + } if (!wpi::contains(serverTeam, '.') && (team = wpi::parse_integer(serverTeam, 10))) { - nt::StartClientTeam(m_inst, team.value(), NT_DEFAULT_PORT); + nt::SetServerTeam(m_inst, team.value(), 0); } else { wpi::SmallVector serverNames; - wpi::SmallVector, 4> servers; + std::vector> servers; wpi::split(serverTeam, serverNames, ',', -1, false); for (auto&& serverName : serverNames) { - servers.emplace_back(serverName, NT_DEFAULT_PORT); + servers.emplace_back(serverName, 0); } - nt::StartClient(m_inst, servers); + nt::SetServer(m_inst, servers); } if (m_dsClient) { - nt::StartDSClient(m_inst, NT_DEFAULT_PORT); + nt::StartDSClient(m_inst, 0); } - } else if (m_mode == 2) { + } else if (m_mode == 3) { nt::StartServer(m_inst, m_iniName.c_str(), m_listenAddress.c_str(), - NT_DEFAULT_PORT); + NT_DEFAULT_PORT3, NT_DEFAULT_PORT4); } } } -NetworkTablesSettings::NetworkTablesSettings(Storage& storage, NT_Inst inst) - : m_mode{storage.GetString("mode"), 0, {"Disabled", "Client", "Server"}}, - m_iniName{storage.GetString("iniName", "networktables.ini")}, +NetworkTablesSettings::NetworkTablesSettings(std::string_view clientName, + Storage& storage, NT_Inst inst) + : m_mode{storage.GetString("mode"), + 0, + {"Disabled", "Client (NT4)", "Client (NT3)", "Server"}}, + m_persistentFilename{ + storage.GetString("persistentFilename", "networktables.json")}, m_serverTeam{storage.GetString("serverTeam")}, m_listenAddress{storage.GetString("listenAddress")}, + m_clientName{storage.GetString("clientName", clientName)}, m_dsClient{storage.GetBool("dsClient", true)} { m_thread.Start(inst); } @@ -101,23 +114,26 @@ void NetworkTablesSettings::Update() { auto thr = m_thread.GetThread(); thr->m_restart = true; thr->m_mode = m_mode.GetValue(); - thr->m_iniName = m_iniName; + thr->m_iniName = m_persistentFilename; thr->m_serverTeam = m_serverTeam; thr->m_listenAddress = m_listenAddress; + thr->m_clientName = m_clientName; thr->m_dsClient = m_dsClient; thr->m_cond.notify_one(); } bool NetworkTablesSettings::Display() { - m_mode.Combo("Mode", m_serverOption ? 3 : 2); + m_mode.Combo("Mode", m_serverOption ? 4 : 3); switch (m_mode.GetValue()) { case 1: + case 2: ImGui::InputText("Team/IP", &m_serverTeam); + ImGui::InputText("Network Identity", &m_clientName); ImGui::Checkbox("Get Address from DS", &m_dsClient); break; - case 2: + case 3: ImGui::InputText("Listen Address", &m_listenAddress); - ImGui::InputText("ini Filename", &m_iniName); + ImGui::InputText("Persistent Filename", &m_persistentFilename); break; default: break; diff --git a/glass/src/libnt/native/cpp/StandardNetworkTables.cpp b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp index 0a5f234422..9e5f2137df 100644 --- a/glass/src/libnt/native/cpp/StandardNetworkTables.cpp +++ b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp @@ -23,7 +23,7 @@ using namespace glass; void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { provider.Register( NTCommandSchedulerModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -34,7 +34,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTCommandSelectorModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -45,7 +45,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTDifferentialDriveModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -56,7 +56,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTFMSModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -66,7 +66,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTDigitalInputModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -77,7 +77,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTDigitalOutputModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -88,7 +88,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTField2DModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [=](Window* win, Model* model, const char* path) { @@ -100,7 +100,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTGyroModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char* path) { @@ -110,7 +110,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTMecanumDriveModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -120,7 +120,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTMechanism2DModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [=](Window* win, Model* model, const char* path) { @@ -132,7 +132,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTPIDControllerModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char* path) { @@ -143,7 +143,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTSpeedControllerModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char* path) { @@ -154,7 +154,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTStringChooserModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { @@ -165,7 +165,7 @@ void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { }); provider.Register( NTSubsystemModel::kType, - [](NT_Inst inst, const char* path) { + [](nt::NetworkTableInstance inst, const char* path) { return std::make_unique(inst, path); }, [](Window* win, Model* model, const char*) { diff --git a/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h index 54dc778841..980e1f5b0a 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h +++ b/glass/src/libnt/native/include/glass/networktables/NTCommandScheduler.h @@ -4,14 +4,18 @@ #pragma once +#include + #include #include #include -#include +#include +#include +#include +#include #include "glass/DataSource.h" -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/CommandScheduler.h" namespace glass { @@ -20,7 +24,7 @@ class NTCommandSchedulerModel : public CommandSchedulerModel { static constexpr const char* kType = "Scheduler"; explicit NTCommandSchedulerModel(std::string_view path); - NTCommandSchedulerModel(NT_Inst instance, std::string_view path); + NTCommandSchedulerModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } const std::vector& GetCurrentCommands() override { @@ -34,14 +38,14 @@ class NTCommandSchedulerModel : public CommandSchedulerModel { bool IsReadOnly() override { return false; } private: - NetworkTablesHelper m_nt; - NT_Entry m_name; - NT_Entry m_commands; - NT_Entry m_ids; - NT_Entry m_cancel; + nt::NetworkTableInstance m_inst; + nt::StringSubscriber m_name; + nt::StringArraySubscriber m_commands; + nt::IntegerArraySubscriber m_ids; + nt::IntegerArrayPublisher m_cancel; std::string m_nameValue; std::vector m_commandsValue; - std::vector m_idsValue; + std::vector m_idsValue; }; } // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h b/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h index c936665915..ab354845e7 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h +++ b/glass/src/libnt/native/include/glass/networktables/NTCommandSelector.h @@ -7,10 +7,11 @@ #include #include -#include +#include +#include +#include #include "glass/DataSource.h" -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/CommandSelector.h" namespace glass { @@ -19,7 +20,7 @@ class NTCommandSelectorModel : public CommandSelectorModel { static constexpr const char* kType = "Command"; explicit NTCommandSelectorModel(std::string_view path); - NTCommandSelectorModel(NT_Inst instance, std::string_view path); + NTCommandSelectorModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } DataSource* GetRunningData() override { return &m_runningData; } @@ -30,9 +31,9 @@ class NTCommandSelectorModel : public CommandSelectorModel { bool IsReadOnly() override { return false; } private: - NetworkTablesHelper m_nt; - NT_Entry m_running; - NT_Entry m_name; + nt::NetworkTableInstance m_inst; + nt::BooleanEntry m_running; + nt::StringSubscriber m_name; DataSource m_runningData; std::string m_nameValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h index 49b3eb0518..5e34669204 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h +++ b/glass/src/libnt/native/include/glass/networktables/NTDifferentialDrive.h @@ -8,10 +8,12 @@ #include #include -#include +#include +#include +#include +#include #include "glass/DataSource.h" -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/Drive.h" namespace glass { @@ -20,7 +22,8 @@ class NTDifferentialDriveModel : public DriveModel { static constexpr const char* kType = "DifferentialDrive"; explicit NTDifferentialDriveModel(std::string_view path); - NTDifferentialDriveModel(NT_Inst instance, std::string_view path); + NTDifferentialDriveModel(nt::NetworkTableInstance instance, + std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } const std::vector& GetWheels() const override { @@ -35,11 +38,11 @@ class NTDifferentialDriveModel : public DriveModel { bool IsReadOnly() override { return !m_controllableValue; } private: - NetworkTablesHelper m_nt; - NT_Entry m_name; - NT_Entry m_controllable; - NT_Entry m_lPercent; - NT_Entry m_rPercent; + nt::NetworkTableInstance m_inst; + nt::StringSubscriber m_name; + nt::BooleanSubscriber m_controllable; + nt::DoubleEntry m_lPercent; + nt::DoubleEntry m_rPercent; std::string m_nameValue; bool m_controllableValue = false; diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h index cd3dfebc1c..7aa9234761 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h +++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h @@ -7,11 +7,12 @@ #include #include -#include +#include +#include +#include #include "glass/DataSource.h" #include "glass/hardware/DIO.h" -#include "glass/networktables/NetworkTablesHelper.h" namespace glass { @@ -21,7 +22,7 @@ class NTDigitalInputModel : public DIOModel { // path is to the table containing ".type", excluding the trailing / explicit NTDigitalInputModel(std::string_view path); - NTDigitalInputModel(NT_Inst inst, std::string_view path); + NTDigitalInputModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } @@ -42,9 +43,9 @@ class NTDigitalInputModel : public DIOModel { bool IsReadOnly() override { return true; } private: - NetworkTablesHelper m_nt; - NT_Entry m_value; - NT_Entry m_name; + nt::NetworkTableInstance m_inst; + nt::BooleanSubscriber m_value; + nt::StringSubscriber m_name; DataSource m_valueData; std::string m_nameValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h index 8ed1ee7207..fb6a15153d 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h +++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h @@ -7,11 +7,12 @@ #include #include -#include +#include +#include +#include #include "glass/DataSource.h" #include "glass/hardware/DIO.h" -#include "glass/networktables/NetworkTablesHelper.h" namespace glass { @@ -21,7 +22,7 @@ class NTDigitalOutputModel : public DIOModel { // path is to the table containing ".type", excluding the trailing / explicit NTDigitalOutputModel(std::string_view path); - NTDigitalOutputModel(NT_Inst inst, std::string_view path); + NTDigitalOutputModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } @@ -42,10 +43,10 @@ class NTDigitalOutputModel : public DIOModel { bool IsReadOnly() override { return !m_controllableValue; } private: - NetworkTablesHelper m_nt; - NT_Entry m_value; - NT_Entry m_name; - NT_Entry m_controllable; + nt::NetworkTableInstance m_inst; + nt::BooleanEntry m_value; + nt::StringSubscriber m_name; + nt::BooleanSubscriber m_controllable; DataSource m_valueData; std::string m_nameValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NTFMS.h b/glass/src/libnt/native/include/glass/networktables/NTFMS.h index b19a9f02da..b8940013e2 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTFMS.h +++ b/glass/src/libnt/native/include/glass/networktables/NTFMS.h @@ -6,10 +6,12 @@ #include -#include +#include +#include +#include +#include #include "glass/DataSource.h" -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/FMS.h" namespace glass { @@ -20,7 +22,7 @@ class NTFMSModel : public FMSModel { // path is to the table containing ".type", excluding the trailing / explicit NTFMSModel(std::string_view path); - NTFMSModel(NT_Inst inst, std::string_view path); + NTFMSModel(nt::NetworkTableInstance inst, std::string_view path); DataSource* GetFmsAttachedData() override { return &m_fmsAttached; } DataSource* GetDsAttachedData() override { return &m_dsAttached; } @@ -52,11 +54,11 @@ class NTFMSModel : public FMSModel { bool IsReadOnly() override { return true; } private: - NetworkTablesHelper m_nt; - NT_Entry m_gameSpecificMessage; - NT_Entry m_alliance; - NT_Entry m_station; - NT_Entry m_controlWord; + nt::NetworkTableInstance m_inst; + nt::StringSubscriber m_gameSpecificMessage; + nt::BooleanSubscriber m_alliance; + nt::IntegerSubscriber m_station; + nt::IntegerSubscriber m_controlWord; DataSource m_fmsAttached; DataSource m_dsAttached; diff --git a/glass/src/libnt/native/include/glass/networktables/NTField2D.h b/glass/src/libnt/native/include/glass/networktables/NTField2D.h index f966e0f01d..257bbca6ec 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTField2D.h +++ b/glass/src/libnt/native/include/glass/networktables/NTField2D.h @@ -10,9 +10,13 @@ #include #include +#include +#include +#include +#include +#include #include -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/Field2D.h" namespace glass { @@ -23,7 +27,7 @@ class NTField2DModel : public Field2DModel { // path is to the table containing ".type", excluding the trailing / explicit NTField2DModel(std::string_view path); - NTField2DModel(NT_Inst inst, std::string_view path); + NTField2DModel(nt::NetworkTableInstance inst, std::string_view path); ~NTField2DModel() override; const char* GetPath() const { return m_path.c_str(); } @@ -40,9 +44,12 @@ class NTField2DModel : public Field2DModel { func) override; private: - NetworkTablesHelper m_nt; std::string m_path; - NT_Entry m_name; + nt::NetworkTableInstance m_inst; + nt::MultiSubscriber m_tableSub; + nt::StringTopic m_nameTopic; + nt::TopicListenerPoller m_topicListener; + nt::ValueListenerPoller m_valueListener; std::string m_nameValue; class ObjectModel; diff --git a/glass/src/libnt/native/include/glass/networktables/NTGyro.h b/glass/src/libnt/native/include/glass/networktables/NTGyro.h index db303a569a..dc91acc451 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTGyro.h +++ b/glass/src/libnt/native/include/glass/networktables/NTGyro.h @@ -7,11 +7,12 @@ #include #include -#include +#include +#include +#include #include "glass/DataSource.h" #include "glass/hardware/Gyro.h" -#include "glass/networktables/NetworkTablesHelper.h" namespace glass { class NTGyroModel : public GyroModel { @@ -19,7 +20,7 @@ class NTGyroModel : public GyroModel { static constexpr const char* kType = "Gyro"; explicit NTGyroModel(std::string_view path); - NTGyroModel(NT_Inst instance, std::string_view path); + NTGyroModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } const char* GetSimDevice() const override { return nullptr; } @@ -32,9 +33,9 @@ class NTGyroModel : public GyroModel { bool IsReadOnly() override { return true; } private: - NetworkTablesHelper m_nt; - NT_Entry m_angle; - NT_Entry m_name; + nt::NetworkTableInstance m_inst; + nt::DoubleSubscriber m_angle; + nt::StringSubscriber m_name; DataSource m_angleData; std::string m_nameValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h index ce7d2342df..38895bc1b4 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h +++ b/glass/src/libnt/native/include/glass/networktables/NTMecanumDrive.h @@ -8,10 +8,12 @@ #include #include -#include +#include +#include +#include +#include #include "glass/DataSource.h" -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/Drive.h" namespace glass { @@ -20,7 +22,7 @@ class NTMecanumDriveModel : public DriveModel { static constexpr const char* kType = "MecanumDrive"; explicit NTMecanumDriveModel(std::string_view path); - NTMecanumDriveModel(NT_Inst instance, std::string_view path); + NTMecanumDriveModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } const std::vector& GetWheels() const override { @@ -35,13 +37,13 @@ class NTMecanumDriveModel : public DriveModel { bool IsReadOnly() override { return !m_controllableValue; } private: - NetworkTablesHelper m_nt; - NT_Entry m_name; - NT_Entry m_controllable; - NT_Entry m_flPercent; - NT_Entry m_frPercent; - NT_Entry m_rlPercent; - NT_Entry m_rrPercent; + nt::NetworkTableInstance m_inst; + nt::StringSubscriber m_name; + nt::BooleanSubscriber m_controllable; + nt::DoubleEntry m_flPercent; + nt::DoubleEntry m_frPercent; + nt::DoubleEntry m_rlPercent; + nt::DoubleEntry m_rrPercent; std::string m_nameValue; bool m_controllableValue = false; diff --git a/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h index 81f7df1cb4..859a1e2390 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h +++ b/glass/src/libnt/native/include/glass/networktables/NTMechanism2D.h @@ -11,9 +11,11 @@ #include #include -#include +#include +#include +#include +#include -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/Mechanism2D.h" namespace glass { @@ -24,7 +26,7 @@ class NTMechanism2DModel : public Mechanism2DModel { // path is to the table containing ".type", excluding the trailing / explicit NTMechanism2DModel(std::string_view path); - NTMechanism2DModel(NT_Inst inst, std::string_view path); + NTMechanism2DModel(nt::NetworkTableInstance inst, std::string_view path); ~NTMechanism2DModel() override; const char* GetPath() const { return m_path.c_str(); } @@ -42,12 +44,14 @@ class NTMechanism2DModel : public Mechanism2DModel { wpi::function_ref func) override; private: - NetworkTablesHelper m_nt; + nt::NetworkTableInstance m_inst; std::string m_path; - - NT_Entry m_name; - NT_Entry m_dimensions; - NT_Entry m_bgColor; + nt::MultiSubscriber m_tableSub; + nt::Topic m_nameTopic; + nt::Topic m_dimensionsTopic; + nt::Topic m_bgColorTopic; + nt::TopicListenerPoller m_topicListener; + nt::ValueListenerPoller m_valueListener; std::string m_nameValue; frc::Translation2d m_dimensionsValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NTPIDController.h b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h index b975641f55..f901f72caa 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTPIDController.h +++ b/glass/src/libnt/native/include/glass/networktables/NTPIDController.h @@ -7,10 +7,12 @@ #include #include -#include +#include +#include +#include +#include #include "glass/DataSource.h" -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/PIDController.h" namespace glass { @@ -19,7 +21,7 @@ class NTPIDControllerModel : public PIDControllerModel { static constexpr const char* kType = "PIDController"; explicit NTPIDControllerModel(std::string_view path); - NTPIDControllerModel(NT_Inst instance, std::string_view path); + NTPIDControllerModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } @@ -38,13 +40,13 @@ class NTPIDControllerModel : public PIDControllerModel { bool IsReadOnly() override { return !m_controllableValue; } private: - NetworkTablesHelper m_nt; - NT_Entry m_name; - NT_Entry m_controllable; - NT_Entry m_p; - NT_Entry m_i; - NT_Entry m_d; - NT_Entry m_setpoint; + nt::NetworkTableInstance m_inst; + nt::StringSubscriber m_name; + nt::BooleanSubscriber m_controllable; + nt::DoubleEntry m_p; + nt::DoubleEntry m_i; + nt::DoubleEntry m_d; + nt::DoubleEntry m_setpoint; DataSource m_pData; DataSource m_iData; diff --git a/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h b/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h index a79c266195..bb6abf3cc5 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h +++ b/glass/src/libnt/native/include/glass/networktables/NTSpeedController.h @@ -7,11 +7,13 @@ #include #include -#include +#include +#include +#include +#include #include "glass/DataSource.h" #include "glass/hardware/SpeedController.h" -#include "glass/networktables/NetworkTablesHelper.h" namespace glass { class NTSpeedControllerModel : public SpeedControllerModel { @@ -19,7 +21,7 @@ class NTSpeedControllerModel : public SpeedControllerModel { static constexpr const char* kType = "Motor Controller"; explicit NTSpeedControllerModel(std::string_view path); - NTSpeedControllerModel(NT_Inst instance, std::string_view path); + NTSpeedControllerModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } const char* GetSimDevice() const override { return nullptr; } @@ -32,10 +34,10 @@ class NTSpeedControllerModel : public SpeedControllerModel { bool IsReadOnly() override { return !m_controllableValue; } private: - NetworkTablesHelper m_nt; - NT_Entry m_value; - NT_Entry m_name; - NT_Entry m_controllable; + nt::NetworkTableInstance m_inst; + nt::DoubleEntry m_value; + nt::StringSubscriber m_name; + nt::BooleanSubscriber m_controllable; DataSource m_valueData; std::string m_nameValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h index 2d806c9778..d770a749e3 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h +++ b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h @@ -7,9 +7,10 @@ #include #include -#include +#include +#include +#include -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/StringChooser.h" namespace glass { @@ -20,7 +21,7 @@ class NTStringChooserModel : public StringChooserModel { // path is to the table containing ".type", excluding the trailing / explicit NTStringChooserModel(std::string_view path); - NTStringChooserModel(NT_Inst inst, std::string_view path); + NTStringChooserModel(nt::NetworkTableInstance inst, std::string_view path); const std::string& GetDefault() override { return m_defaultValue; } const std::string& GetSelected() override { return m_selectedValue; } @@ -29,21 +30,18 @@ class NTStringChooserModel : public StringChooserModel { return m_optionsValue; } - void SetDefault(std::string_view val) override; void SetSelected(std::string_view val) override; - void SetActive(std::string_view val) override; - void SetOptions(wpi::span val) override; void Update() override; bool Exists() override; bool IsReadOnly() override { return false; } private: - NetworkTablesHelper m_nt; - NT_Entry m_default; - NT_Entry m_selected; - NT_Entry m_active; - NT_Entry m_options; + nt::NetworkTableInstance m_inst; + nt::StringSubscriber m_default; + nt::StringEntry m_selected; + nt::StringSubscriber m_active; + nt::StringArraySubscriber m_options; std::string m_defaultValue; std::string m_selectedValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h index c5862cfbd9..6c1ee3a2fd 100644 --- a/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h +++ b/glass/src/libnt/native/include/glass/networktables/NTSubsystem.h @@ -7,10 +7,10 @@ #include #include -#include +#include +#include #include "glass/DataSource.h" -#include "glass/networktables/NetworkTablesHelper.h" #include "glass/other/Subsystem.h" namespace glass { @@ -19,7 +19,7 @@ class NTSubsystemModel : public SubsystemModel { static constexpr const char* kType = "Subsystem"; explicit NTSubsystemModel(std::string_view path); - NTSubsystemModel(NT_Inst instance, std::string_view path); + NTSubsystemModel(nt::NetworkTableInstance inst, std::string_view path); const char* GetName() const override { return m_nameValue.c_str(); } const char* GetDefaultCommand() const override { @@ -34,10 +34,10 @@ class NTSubsystemModel : public SubsystemModel { bool IsReadOnly() override { return true; } private: - NetworkTablesHelper m_nt; - NT_Entry m_name; - NT_Entry m_defaultCommand; - NT_Entry m_currentCommand; + nt::NetworkTableInstance m_inst; + nt::StringSubscriber m_name; + nt::StringSubscriber m_defaultCommand; + nt::StringSubscriber m_currentCommand; std::string m_nameValue; std::string m_defaultCommandValue; diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h index dcd1b4f517..31f5ca3082 100644 --- a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h +++ b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h @@ -4,13 +4,20 @@ #pragma once +#include +#include #include #include #include +#include #include +#include +#include +#include #include #include +#include #include "glass/Model.h" #include "glass/View.h" @@ -21,28 +28,81 @@ class DataSource; class NetworkTablesModel : public Model { public: - struct Entry { - explicit Entry(nt::EntryNotification&& event); + struct SubscriberOptions { + float periodic = 0.1f; + bool immediate = false; + bool sendAll = false; + bool prefixMatch = false; + // std::string otherStr; + }; - void UpdateValue(); + struct TopicPublisher { + std::string client; + uint64_t pubuid; + }; - /** Entry handle. */ - NT_Entry entry; + struct TopicSubscriber { + std::string client; + uint64_t subuid; + SubscriberOptions options; + }; - /** Entry name. */ - std::string name; + struct EntryValueTreeNode; - /** The value. */ - std::shared_ptr value; + struct ValueSource { + void UpdateFromValue(nt::Value&& v, std::string_view name, + std::string_view typeStr); - /** Flags. */ - unsigned int flags = 0; + /** The latest value. */ + nt::Value value; /** String representation of the value (for arrays / complex values). */ std::string valueStr; /** Data source (for numeric values). */ std::unique_ptr source; + + /** Children of this node, sorted by name/index */ + std::vector valueChildren; + + /** Whether or not the children represent a map */ + bool valueChildrenMap = false; + }; + + struct EntryValueTreeNode : public ValueSource { + /** Short name (e.g. of just this node) */ + std::string name; + + /** Full path */ + std::string path; + }; + + struct Entry : public ValueSource { + Entry() = default; + Entry(const Entry&) = delete; + Entry& operator=(const Entry&) = delete; + ~Entry(); + + void UpdateTopic(nt::TopicNotification&& event) { + UpdateInfo(std::move(event.info)); + } + void UpdateInfo(nt::TopicInfo&& info_); + + /** Topic information. */ + nt::TopicInfo info; + + /** JSON representation of the topic properties. */ + wpi::json properties; + + /** Specific common property flags. */ + bool persistent{false}; + bool retained{false}; + + /** Publisher (created when the value changes). */ + NT_Publisher publisher{0}; + + std::vector publishers; + std::vector subscribers; }; struct TreeNode { @@ -64,45 +124,97 @@ class NetworkTablesModel : public Model { std::vector children; }; + struct ClientPublisher { + int64_t uid = -1; + std::string topic; + }; + + struct ClientSubscriber { + int64_t uid = -1; + std::vector topics; + std::string topicsStr; + SubscriberOptions options; + }; + + struct Client { + std::string id; + std::string conn; + std::string version; + std::vector publishers; + std::vector subscribers; + + void UpdatePublishers(wpi::span data); + void UpdateSubscribers(wpi::span data); + }; + NetworkTablesModel(); - explicit NetworkTablesModel(NT_Inst inst); - ~NetworkTablesModel() override; + explicit NetworkTablesModel(nt::NetworkTableInstance inst); void Update() override; bool Exists() override; - NT_Inst GetInstance() { return m_inst; } - const std::vector& GetEntries() { return m_sortedEntries; } - const std::vector& GetTreeRoot() { return m_root; } + nt::NetworkTableInstance GetInstance() { return m_inst; } + const std::vector& GetEntries() const { return m_sortedEntries; } + const std::vector& GetTreeRoot() const { return m_root; } + const std::vector& GetPersistentTreeRoot() const { + return m_persistentRoot; + } + const std::vector& GetRetainedTreeRoot() const { + return m_retainedRoot; + } + const std::vector& GetTransitoryTreeRoot() const { + return m_transitoryRoot; + } + const std::map>& GetClients() const { + return m_clients; + } + const Client& GetServer() const { return m_server; } + Entry* GetEntry(std::string_view name); + Entry* AddEntry(NT_Topic topic); private: - NT_Inst m_inst; - NT_EntryListenerPoller m_poller; - wpi::DenseMap> m_entries; + void RebuildTree(); + void RebuildTreeImpl(std::vector* tree, int category); + void UpdateClients(wpi::span data); + + nt::NetworkTableInstance m_inst; + NT_MultiSubscriber m_subscriber; + nt::TopicListenerPoller m_topicPoller; + nt::ValueListenerPoller m_valuePoller; + wpi::DenseMap> m_entries; // sorted by name std::vector m_sortedEntries; std::vector m_root; + std::vector m_persistentRoot; + std::vector m_retainedRoot; + std::vector m_transitoryRoot; + + std::map> m_clients; + Client m_server; }; using NetworkTablesFlags = int; -static constexpr const int kNetworkTablesFlags_PrecisionBitShift = 6; +static constexpr const int kNetworkTablesFlags_PrecisionBitShift = 9; enum NetworkTablesFlags_ { NetworkTablesFlags_TreeView = 1 << 0, - NetworkTablesFlags_ReadOnly = 1 << 1, - NetworkTablesFlags_ShowConnections = 1 << 2, - NetworkTablesFlags_ShowFlags = 1 << 3, - NetworkTablesFlags_ShowTimestamp = 1 << 4, - NetworkTablesFlags_CreateNoncanonicalKeys = 1 << 5, + NetworkTablesFlags_CombinedView = 1 << 1, + NetworkTablesFlags_ReadOnly = 1 << 2, + NetworkTablesFlags_ShowSpecial = 1 << 3, + NetworkTablesFlags_ShowProperties = 1 << 4, + NetworkTablesFlags_ShowTimestamp = 1 << 5, + NetworkTablesFlags_ShowServerTimestamp = 1 << 6, + NetworkTablesFlags_CreateNoncanonicalKeys = 1 << 7, NetworkTablesFlags_Precision = 0xff << kNetworkTablesFlags_PrecisionBitShift, - NetworkTablesFlags_Default = (1 & ~NetworkTablesFlags_ReadOnly & - ~NetworkTablesFlags_CreateNoncanonicalKeys) | + NetworkTablesFlags_Default = NetworkTablesFlags_TreeView | (6 << kNetworkTablesFlags_PrecisionBitShift), }; +void DisplayNetworkTablesInfo(NetworkTablesModel* model); + void DisplayNetworkTables( NetworkTablesModel* model, NetworkTablesFlags flags = NetworkTablesFlags_Default); @@ -120,9 +232,11 @@ class NetworkTablesFlagsSettings { private: bool* m_pTreeView = nullptr; - bool* m_pShowConnections = nullptr; - bool* m_pShowFlags = nullptr; + bool* m_pCombinedView = nullptr; + bool* m_pShowSpecial = nullptr; + bool* m_pShowProperties = nullptr; bool* m_pShowTimestamp = nullptr; + bool* m_pShowServerTimestamp = nullptr; bool* m_pCreateNoncanonicalKeys = nullptr; int* m_pPrecision = nullptr; NetworkTablesFlags m_defaultFlags; // NOLINT diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h deleted file mode 100644 index aba32525c8..0000000000 --- a/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h +++ /dev/null @@ -1,55 +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. - -#pragma once - -#include -#include - -#include - -namespace glass { - -class NetworkTablesHelper { - public: - explicit NetworkTablesHelper(NT_Inst inst); - ~NetworkTablesHelper(); - - NetworkTablesHelper(const NetworkTablesHelper&) = delete; - NetworkTablesHelper& operator=(const NetworkTablesHelper&) = delete; - - NT_Inst GetInstance() const { return m_inst; } - NT_EntryListenerPoller GetPoller() const { return m_poller; } - - NT_Entry GetEntry(std::string_view name) const { - return nt::GetEntry(m_inst, name); - } - - static constexpr int kDefaultListenerFlags = - NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE | - NT_NOTIFY_IMMEDIATE; - - NT_EntryListener AddListener(NT_Entry entry, - unsigned int flags = kDefaultListenerFlags) { - return nt::AddPolledEntryListener(m_poller, entry, flags); - } - - NT_EntryListener AddListener(std::string_view prefix, - unsigned int flags = kDefaultListenerFlags) { - return nt::AddPolledEntryListener(m_poller, prefix, flags); - } - - std::vector PollListener() { - bool timedOut = false; - return nt::PollEntryListener(m_poller, 0, &timedOut); - } - - bool IsConnected() const; - - private: - NT_Inst m_inst; - NT_EntryListenerPoller m_poller; -}; - -} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h index a8f0f9b4e1..da02b10c4a 100644 --- a/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h +++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h @@ -9,13 +9,16 @@ #include #include -#include -#include +#include +#include +#include +#include +#include +#include #include #include "glass/Model.h" #include "glass/Provider.h" -#include "glass/networktables/NetworkTablesHelper.h" namespace glass { @@ -23,9 +26,10 @@ class Window; namespace detail { struct NTProviderFunctions { - using Exists = std::function; - using CreateModel = - std::function(NT_Inst inst, const char* path)>; + using Exists = + std::function; + using CreateModel = std::function( + nt::NetworkTableInstance inst, const char* path)>; using ViewExists = std::function; using CreateView = std::function(Window*, Model*, const char* path)>; @@ -41,14 +45,14 @@ class NetworkTablesProvider : private Provider { using Provider::CreateViewFunc; explicit NetworkTablesProvider(Storage& storage); - NetworkTablesProvider(Storage& storage, NT_Inst inst); + NetworkTablesProvider(Storage& storage, nt::NetworkTableInstance inst); /** * Get the NetworkTables instance being used for this provider. * * @return NetworkTables instance */ - NT_Inst GetInstance() const { return m_nt.GetInstance(); } + nt::NetworkTableInstance GetInstance() const { return m_inst; } /** * Perform global initialization. This should be called prior to @@ -74,8 +78,10 @@ class NetworkTablesProvider : private Provider { private: void Update() override; - NetworkTablesHelper m_nt; - NT_EntryListener m_listener{0}; + nt::NetworkTableInstance m_inst; + nt::TopicListenerPoller m_topicPoller; + NT_TopicListener m_topicListener{0}; + nt::ValueListenerPoller m_valuePoller; // cached mapping from table name to type string Storage& m_typeCache; @@ -88,17 +94,25 @@ class NetworkTablesProvider : private Provider { // mapping from .type string to model/view creators wpi::StringMap m_typeMap; + struct SubListener { + nt::StringSubscriber subscriber; + NT_ValueListener listener; + }; + + // mapping from .type topic to subscriber/listener + wpi::DenseMap m_topicMap; + struct Entry : public ModelEntry { - Entry(NT_Entry typeEntry, std::string_view name, const Builder& builder) - : ModelEntry{name, [](NT_Inst, const char*) { return true; }, + Entry(nt::Topic typeTopic, std::string_view name, const Builder& builder) + : ModelEntry{name, [](auto, const char*) { return true; }, builder.createModel}, - typeEntry{typeEntry} {} - NT_Entry typeEntry; + typeTopic{typeTopic} {} + nt::Topic typeTopic; }; void Show(ViewEntry* entry, Window* window) override; - ViewEntry* GetOrCreateView(const Builder& builder, NT_Entry typeEntry, + ViewEntry* GetOrCreateView(const Builder& builder, nt::Topic typeTopic, std::string_view name); }; diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h index a1a1c9334f..12b55e6631 100644 --- a/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h +++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesSettings.h @@ -22,7 +22,7 @@ class Storage; class NetworkTablesSettings { public: - explicit NetworkTablesSettings(Storage& storage, + explicit NetworkTablesSettings(std::string_view clientName, Storage& storage, NT_Inst inst = nt::GetDefaultInstance()); /** @@ -37,9 +37,10 @@ class NetworkTablesSettings { bool m_restart = true; bool m_serverOption = true; EnumSetting m_mode; - std::string& m_iniName; + std::string& m_persistentFilename; std::string& m_serverTeam; std::string& m_listenAddress; + std::string& m_clientName; bool& m_dsClient; class Thread : public wpi::SafeThread { @@ -54,6 +55,7 @@ class NetworkTablesSettings { std::string m_iniName; std::string m_serverTeam; std::string m_listenAddress; + std::string m_clientName; bool m_dsClient; }; wpi::SafeThreadOwner m_thread; diff --git a/myRobot/build.gradle b/myRobot/build.gradle index f3e56a9b45..6cd26e8531 100644 --- a/myRobot/build.gradle +++ b/myRobot/build.gradle @@ -167,9 +167,9 @@ model { lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' lib project: ':cameraserver', library: 'cameraserver', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(binary, 'shared') + project(':ntcore').addNtcoreJniDependency(binary) lib project: ':cscore', library: 'cscore', linkage: 'shared' - lib project: ':ntcore', library: 'ntcoreJNIShared', linkage: 'shared' lib project: ':cscore', library: 'cscoreJNIShared', linkage: 'shared' lib project: ':wpimath', library: 'wpimathJNIShared', linkage: 'shared' lib project: ':wpinet', library: 'wpinetJNIShared', linkage: 'shared' @@ -215,7 +215,7 @@ model { lib project: ':wpilibc', library: 'wpilibc', linkage: 'static' lib project: ':wpimath', library: 'wpimath', linkage: 'static' lib project: ':cameraserver', library: 'cameraserver', linkage: 'static' - lib project: ':ntcore', library: 'ntcore', linkage: 'static' + project(':ntcore').addNtcoreDependency(binary, 'static') lib project: ':cscore', library: 'cscore', linkage: 'static' project(':hal').addHalDependency(binary, 'static') lib project: ':wpinet', library: 'wpinet', linkage: 'static' diff --git a/ntcore/CMakeLists.txt b/ntcore/CMakeLists.txt index 0edc4ffee7..df25b57f5f 100644 --- a/ntcore/CMakeLists.txt +++ b/ntcore/CMakeLists.txt @@ -3,14 +3,29 @@ project(ntcore) 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 $ + $ $) wpilib_target_warnings(ntcore) target_compile_features(ntcore PUBLIC cxx_std_17) @@ -38,10 +53,11 @@ if (WITH_JAVA) 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 +94,9 @@ if (WITH_JAVA) 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 145d589ada..bf48619fb3 100644 --- a/ntcore/build.gradle +++ b/ntcore/build.gradle @@ -1,6 +1,291 @@ +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() + replacements.put("types", jsonTypes) + def output = jinjava.render(template, replacements) + new File(ntcoreJavaTypesOutputDir, outfn).write(output) + } else { + jsonTypes.each { Map 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 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() + 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() + 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() + 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() + 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() + 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" @@ -18,6 +303,18 @@ model { } } +sourceSets.main.java.srcDir "${buildDir}/generated/main/java" +compileJava.dependsOn ntcoreGenerateJavaTypes + +cppHeadersZip { + dependsOn ntcoreGenerateCppTypes + dependsOn ntcoreGenerateCppHandleHeader + dependsOn ntcoreGenerateCHandleHeader + from(generatedHeaders) { + into '/' + } +} + Action> symbolFilter = { symbols -> symbols.removeIf({ !it.startsWith('NT_') }) } as Action>; diff --git a/ntcore/doc/networktables4.adoc b/ntcore/doc/networktables4.adoc new file mode 100644 index 0000000000..6e5e3c9b87 --- /dev/null +++ b/ntcore/doc/networktables4.adoc @@ -0,0 +1,819 @@ += Network Tables Protocol Specification, Version 4.0 +WPILib Developers +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]] +* <> + +[[cristians-algorithm]] +* Cristian's algorithm, https://en.wikipedia.org/wiki/Cristian%27s_algorithm + +[[design]] +== Design + +Both text and binary <> frames are used for transport. Text frames are <>, and binary frames are <>. 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 <>. + +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 <>. + +[[topic-names]] +=== Topic Names + +Topic have UTF-8 string names. Names starting with `$` are reserved for use by the server (see <>). 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 <>). + +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 <> 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 <> 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. <> or <>) 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 <> 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 <> and <> messages as required for the application when a new connection is established, and not using old topic IDs, but rather waiting for new <> 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 <> 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 <>). + +[[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 +|<>|`msgpack`|Connected clients +|<`>>|`msgpack`|Client `` subscriptions +|<>|`msgpack`|Server subscriptions +|<`>>|`msgpack`|Subscriptions to `` +|<`>>|`msgpack`|Client `` publishers +|<>|`msgpack`|Server publishers +|<`>>|`msgpack`|Publishers to `` +|=== + +[[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$`) + +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 +|<> +|=== + +[[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$`) + +The server shall update this topic when a client subscribes or unsubscribes to ``. + +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 +|<> +|=== + +[[meta-client-pub]] +==== Client Publishers (`$clientpub$`) + +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$`) + +The server shall update this topic when a client publishes or unpublishes to ``. + +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/`, where `` is an arbitrary string representing the client name. The client name must be unique (for a particular server). Servers shall reject duplicate connections to the same resource name by responding with HTTP Error Code 409 (Conflict). Clients should provide a way to specify the resource name (in particular, the client name portion) and should provide a mechanism to make the name unique (e.g. by suffixing the name with a unique identifier). + +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 <>. + +[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 <> 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 <> 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) + +|<> +|Publish Request +|Client to Server +|<> + +|<> +|Publish Release +|Client to Server +|<> (if topic deleted) + +|<> +|Set Properties +|Client to Server +|<> + +4+|Value/Subscription Messages (Client to Server) + +|<> +|Subscribe +|Client to Server +|<> (once topic is announced) + +|<> +|Unsubscribe +|Client to Server +|--- + +4+|Announcement Messages (Server to Client) + +|<> +|Topic Announcement +|Server to Client +|--- + +|<> +|Topic Removed +|Server to Client +|--- + +|<> +|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 <>, 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 <> 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 <> message contains the actual topic properties. Clients can use the <> 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 <> to all clients who have been sent a previous <> 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 <> message +|=== + +[[msg-setproperties]] +==== Set Properties Message (`setproperties`) + +Sent from a client to the server to change properties (see <>) for a given topic. The server will send a corresponding <> 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 <> 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 +|<> +|=== + +[[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 <> 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 <> 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 <> message, the Publisher UID provided in that message. Otherwise absent. + +|`properties` +|Map +|Properties +|Topic <> +|=== + +[[msg-unannounce]] +==== Topic Removed Message (`unannounce`) + +The server shall send this message when a previously announced (via a <>) 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 <>) topic has its properties changed (via <>). + +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 <> message from the same client. Otherwise absent. + +|`update` +|Map +|Properties to update (from <>) +|=== + +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 <> 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 <> 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 <> 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 0000000000..ece7df2988 --- /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 f86301867a..0ef12d6571 100644 --- a/ntcore/src/dev/native/cpp/main.cpp +++ b/ntcore/src/dev/native/cpp/main.cpp @@ -2,14 +2,110 @@ // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. -#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include #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& times) { + std::sort(times.begin(), times.end()); + int64_t min = times[0]; + int64_t max = times[times.size() - 1]; + double mean = + static_cast(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(); + nt::SetNetworkIdentity(server, "server"); + nt::SetNetworkIdentity(client, "client"); + + // connect client and server + nt::StartServer(server, "bench.json", "127.0.0.1", 0, 10000); + nt::StartClient4(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", + {{nt::PubSubOption::KeepDuplicates(true), + nt::PubSubOption::SendAll(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 flushTimes; + flushTimes.reserve(100); + + std::vector 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(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 0000000000..6c26463b7d --- /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 + +#include + +#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 FromJavaBooleanArray(JNIEnv* env, jbooleanArray jarr) { + CriticalJBooleanArrayRef ref{env, jarr}; + if (!ref) { + return {}; + } + wpi::span elements{ref}; + size_t len = elements.size(); + std::vector arr; + arr.reserve(len); + for (size_t i = 0; i < len; ++i) { + arr.push_back(elements[i]); + } + return arr; +} + +static std::vector FromJavaStringArray(JNIEnv* env, jobjectArray jarr) { + size_t len = env->GetArrayLength(jarr); + std::vector arr; + arr.reserve(len); + for (size_t i = 0; i < len; ++i) { + JLocal elem{ + env, static_cast(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, "", "(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(value.time), + static_cast(value.serverTime), val.obj()); +{%- else %} + return env->NewObject(timestamped{{ t.TypeName }}Cls, constructor, + static_cast(value.time), + static_cast(value.serverTime), + {{ t.jni.ToJavaBegin }}value.value{{ t.jni.ToJavaEnd }}); +{%- endif %} +} +{% endfor %} +{%- for t in types %} +static jobjectArray MakeJObject(JNIEnv* env, + wpi::span 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 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, wpi::span 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 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 0000000000..342d444cee --- /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 +static inline wpi::span 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 ConvertFromC(const NT_String* arr, size_t size) { + std::vector 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(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 0000000000..ba8bca7c0d --- /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 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 0000000000..d367de5fa1 --- /dev/null +++ b/ntcore/src/generate/include/networktables/Topic.h.jinja @@ -0,0 +1,437 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +{{ cpp.INCLUDES }} +#include +#include + +#include + +#include "networktables/Topic.h" + +namespace wpi { +template +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& 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& 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& 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& 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 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. + * + *

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, + wpi::span options = {}); +{%- if TypeString %} + /** + * Create a new subscriber to the topic, with specific type string. + * + *

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, + wpi::span options = {}); +{% 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 %}wpi::span options = {}); + + /** + * Create a new publisher to the topic, with type string and initial + * properties. + * + * The publisher is only active as long as the returned object + * is not destroyed. + * + * @note It is not possible to publish two different data types to the same + * topic. Conflicts between publishers are typically resolved by the + * server on a first-come, first-served basis. Any published values that + * do not match the topic's data type are dropped (ignored). To determine + * if the data type matches, use the appropriate Topic functions. + * + * @param typeString type string + * @param properties JSON properties + * @param options publish options + * @return publisher + */ + [[nodiscard]] + PublisherType PublishEx(std::string_view typeString, + const wpi::json& properties, wpi::span options = {}); + + /** + * Create a new entry for the topic. + * + * Entries act as a combination of a subscriber and a weak publisher. The + * subscriber is active as long as the entry is not destroyed. The publisher + * is created when the entry is first written to, and remains active until + * either Unpublish() is called or the entry is destroyed. + * + * @note It is not possible to use two different data types with the same + * topic. Conflicts between publishers are typically resolved by the + * server on a first-come, first-served basis. Any published values that + * do not match the topic's data type are dropped (ignored), and the entry + * will show no new values if the data type does not match. To determine + * if the data type matches, use the appropriate Topic functions. + * +{%- 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, + wpi::span options = {}); +{%- 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, + wpi::span options = {}); +{% 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 0000000000..e996ed39ca --- /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 +{{ 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, + wpi::span 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, + wpi::span 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 %}wpi::span 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, wpi::span 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, + wpi::span 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, + wpi::span 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 0000000000..d5b24481d1 --- /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 + +#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 0000000000..8ca926e7e0 --- /dev/null +++ b/ntcore/src/generate/include/ntcore_cpp_types.h.jinja @@ -0,0 +1,155 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include +#include + +#include + +#include "ntcore_c.h" + +namespace wpi { +template +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 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 0000000000..cbaa782c20 --- /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. + * + *

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 0000000000..43b31e4a43 --- /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 0000000000..e4296d7d9c --- /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 0000000000..d747f1751a --- /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 { + /** + * 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 0000000000..63ecebcf20 --- /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 { + /** + * 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. + * + *

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 0000000000..783ec6fac1 --- /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. + * + *

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. + * + *

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 0000000000..f1c6a908dc --- /dev/null +++ b/ntcore/src/generate/java/NetworkTableInstance.java.jinja @@ -0,0 +1,852 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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.datalog.DataLog; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +/** + * NetworkTables Instance. + * + *

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. + * + *

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. + * + *

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 kNetModeClient3 = 0x02; + public static final int kNetModeClient4 = 0x04; + public static final int kNetModeStarting = 0x08; + public static final int kNetModeLocal = 0x10; + + /** 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) { + 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 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 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 m_topics = new ConcurrentHashMap<>(); + private final ConcurrentMap 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 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 final ReentrantLock m_connectionListenerLock = new ReentrantLock(); + private final Map> m_connectionListeners = + new HashMap<>(); + private int m_connectionListenerPoller; + + @SuppressWarnings("PMD.AvoidCatchingThrowable") + private void startConnectionListenerThread() { + var connectionListenerThread = + new Thread( + () -> { + boolean wasInterrupted = false; + while (!Thread.interrupted()) { + try { + WPIUtilJNI.waitForObject(m_connectionListenerPoller); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + // don't try to destroy poller, as its handle is likely no longer valid + wasInterrupted = true; + break; + } + ConnectionNotification[] events = + NetworkTablesJNI.readConnectionListenerQueue(this, m_connectionListenerPoller); + for (ConnectionNotification event : events) { + Consumer 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 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); + } + + /* + * 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.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. */ + public void startClient3() { + NetworkTablesJNI.startClient3(m_handle); + } + + /** Starts a NT4 client. Use SetServer or SetServerTeam to set the server name and port. */ + public void startClient4() { + NetworkTablesJNI.startClient4(m_handle); + } + + /** 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); + } + + /** + * 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); + } + + private final ReentrantLock m_loggerLock = new ReentrantLock(); + private final Map> m_loggers = new HashMap<>(); + private int m_loggerPoller; + + @SuppressWarnings("PMD.AvoidCatchingThrowable") + private void startLogThread() { + var loggerThread = + new Thread( + () -> { + boolean wasInterrupted = false; + while (!Thread.interrupted()) { + try { + WPIUtilJNI.waitForObject(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; + } + LogMessage[] events = NetworkTablesJNI.readLoggerQueue(this, m_loggerPoller); + for (LogMessage event : events) { + Consumer 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); + } + } 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 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); + } + + @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/generate/java/NetworkTableValue.java.jinja b/ntcore/src/generate/java/NetworkTableValue.java.jinja new file mode 100644 index 0000000000..d2c8d11fe2 --- /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 0000000000..d7f2c9f516 --- /dev/null +++ b/ntcore/src/generate/java/NetworkTablesJNI.java.jinja @@ -0,0 +1,301 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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.concurrent.atomic.AtomicBoolean; + +public final class NetworkTablesJNI { + static boolean libraryLoaded = false; + static RuntimeLoader 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 int[] pubSubOptionTypes(PubSubOption... options) { + int[] rv = new int[options.length]; + for (int i = 0; i < options.length; i++) { + rv[i] = options[i].m_type; + } + return rv; + } + + private static double[] pubSubOptionValues(PubSubOption... options) { + double[] rv = new double[options.length]; + for (int i = 0; i < options.length; i++) { + rv[i] = options[i].m_value; + } + return rv; + } + + 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); + + private static native int getEntry( + int topic, int type, String typeStr, int[] optionTypes, double[] optionValues); + + public static int getEntry(int topic, int type, String typeStr, PubSubOption... options) { + return getEntry(topic, type, typeStr, pubSubOptionTypes(options), pubSubOptionValues(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); + + private static native int subscribe( + int topic, int type, String typeStr, int[] optionTypes, double[] optionValues); + + public static int subscribe(int topic, int type, String typeStr, PubSubOption... options) { + return subscribe(topic, type, typeStr, pubSubOptionTypes(options), pubSubOptionValues(options)); + } + + public static native void unsubscribe(int sub); + + private static native int publish( + int topic, int type, String typeStr, int[] optionTypes, double[] optionValues); + + public static int publish(int topic, int type, String typeStr, PubSubOption... options) { + return publish(topic, type, typeStr, pubSubOptionTypes(options), pubSubOptionValues(options)); + } + + private static native int publishEx( + int topic, int type, String typeStr, String properties, int[] optionTypes, double[] optionValues); + + public static int publishEx(int topic, int type, String typeStr, String properties, PubSubOption... options) { + return publishEx(topic, type, typeStr, properties, pubSubOptionTypes(options), pubSubOptionValues(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); + + private static native int subscribeMultiple( + int inst, String[] prefixes, int[] optionTypes, double[] optionValues); + + public static int subscribeMultiple(int inst, String[] prefixes, PubSubOption... options) { + return subscribeMultiple( + inst, prefixes, pubSubOptionTypes(options), pubSubOptionValues(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 createTopicListenerPoller(int inst); + + public static native void destroyTopicListenerPoller(int poller); + + public static native int addPolledTopicListener(int poller, String[] prefixes, int flags); + + public static native int addPolledTopicListener(int poller, int handle, int flags); + + public static native TopicNotification[] readTopicListenerQueue( + NetworkTableInstance inst, int poller); + + public static native void removeTopicListener(int topicListener); + + public static native int createValueListenerPoller(int inst); + + public static native void destroyValueListenerPoller(int poller); + + public static native int addPolledValueListener(int poller, int subentry, int flags); + + public static native ValueNotification[] readValueListenerQueue( + NetworkTableInstance inst, int poller); + + public static native void removeValueListener(int valueListener); + + 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[] readConnectionListenerQueue( + NetworkTableInstance inst, int poller); + + public static native void removeConnectionListener(int connListener); + + 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 port3, int port4); + + public static native void stopServer(int inst); + + public static native void startClient3(int inst); + + public static native void startClient4(int inst); + + 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 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 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[] readLoggerQueue(NetworkTableInstance inst, int poller); + + public static native void removeLogger(int logger); +} diff --git a/ntcore/src/generate/java/Publisher.java.jinja b/ntcore/src/generate/java/Publisher.java.jinja new file mode 100644 index 0000000000..19ead36b6d --- /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 0000000000..0ea09a36e6 --- /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. + * + *

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. + * + *

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 0000000000..288af81b79 --- /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 0000000000..307291df45 --- /dev/null +++ b/ntcore/src/generate/java/Topic.java.jinja @@ -0,0 +1,222 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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. + * + *

The subscriber is only active as long as the returned object + * is not closed. + * + *

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. + * + *

The subscriber is only active as long as the returned object + * is not closed. + * + *

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. + * + *

The publisher is only active as long as the returned object + * is not closed. + * + *

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. + * + *

The publisher is only active as long as the returned object + * is not closed. + * + *

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 + */ + 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 0000000000..6248fdf023 --- /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(", + "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(", + "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(", + "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(", + "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 \n#include \n#include ", + "SmallRetType": "std::string_view", + "SmallElemType": "char" + }, + "java": { + "ValueType": "String", + "EmptyValue": "\"\"", + "FunctionTypeSuffix": "", + "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", + "ParamType": "wpi::span", + "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()", + "TYPE_NAME": "RAW", + "INCLUDES": "#include ", + "SmallRetType": "wpi::span", + "SmallElemType": "uint8_t" + }, + "java": { + "ValueType": "byte[]", + "EmptyValue": "new byte[] {}", + "FunctionTypeSuffix": "", + "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", + "ParamType": "wpi::span", + "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()", + "TYPE_NAME": "BOOLEAN_ARRAY", + "INCLUDES": "#include ", + "SmallRetType": "wpi::span", + "SmallElemType": "int" + }, + "java": { + "ValueType": "boolean[]", + "WrapValueType": "Boolean[]", + "EmptyValue": "new boolean[] {}", + "FunctionTypeSuffix": "", + "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", + "ParamType": "wpi::span", + "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()", + "TYPE_NAME": "INTEGER_ARRAY", + "INCLUDES": "#include ", + "SmallRetType": "wpi::span", + "SmallElemType": "int64_t" + }, + "java": { + "ValueType": "long[]", + "WrapValueType": "Long[]", + "EmptyValue": "new long[] {}", + "FunctionTypeSuffix": "", + "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", + "ParamType": "wpi::span", + "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()", + "TYPE_NAME": "FLOAT_ARRAY", + "INCLUDES": "#include ", + "SmallRetType": "wpi::span", + "SmallElemType": "float" + }, + "java": { + "ValueType": "float[]", + "WrapValueType": "Float[]", + "EmptyValue": "new float[] {}", + "FunctionTypeSuffix": "", + "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", + "ParamType": "wpi::span", + "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()", + "TYPE_NAME": "DOUBLE_ARRAY", + "INCLUDES": "#include ", + "SmallRetType": "wpi::span", + "SmallElemType": "double" + }, + "java": { + "ValueType": "double[]", + "WrapValueType": "Double[]", + "EmptyValue": "new double[] {}", + "FunctionTypeSuffix": "", + "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", + "ParamType": "wpi::span", + "DefaultValueCopy": "defaultValue.begin(), defaultValue.end()", + "TYPE_NAME": "STRING_ARRAY", + "INCLUDES": "#include " + }, + "java": { + "ValueType": "String[]", + "EmptyValue": "new String[] {}", + "FunctionTypeSuffix": "", + "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/ConnectionListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionListener.java new file mode 100644 index 0000000000..2e598d51f7 --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionListener.java @@ -0,0 +1,141 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +/** Connection listener. This calls back to a callback function when a connection change occurs. */ +public final class ConnectionListener implements AutoCloseable { + /** + * Create a listener for connection changes. + * + * @param inst Instance + * @param immediateNotify if notification should be immediately created for existing connections + * @param listener Listener function + */ + public ConnectionListener( + NetworkTableInstance inst, + boolean immediateNotify, + Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = inst; + s_poller = NetworkTablesJNI.createConnectionListenerPoller(inst.getHandle()); + startThread(); + } + m_handle = NetworkTablesJNI.addPolledConnectionListener(s_poller, immediateNotify); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + @Override + public synchronized void close() { + if (m_handle != 0) { + s_lock.lock(); + try { + s_listeners.remove(m_handle); + } finally { + s_lock.unlock(); + } + NetworkTablesJNI.removeConnectionListener(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; + } + + private int m_handle; + + private static final ReentrantLock s_lock = new ReentrantLock(); + private static final Map> s_listeners = new HashMap<>(); + private static Thread s_thread; + private static NetworkTableInstance s_inst; + private static int s_poller; + private static boolean s_waitQueue; + private static final Condition s_waitQueueCond = s_lock.newCondition(); + + private static void startThread() { + s_thread = + new Thread( + () -> { + boolean wasInterrupted = false; + while (!Thread.interrupted()) { + try { + WPIUtilJNI.waitForObject(s_poller); + } catch (InterruptedException ex) { + s_lock.lock(); + try { + if (s_waitQueue) { + s_waitQueue = false; + s_waitQueueCond.signalAll(); + continue; + } + } finally { + s_lock.unlock(); + } + Thread.currentThread().interrupt(); + // don't try to destroy poller, as its handle is likely no longer valid + wasInterrupted = true; + break; + } + for (ConnectionNotification event : + NetworkTablesJNI.readConnectionListenerQueue(s_inst, s_poller)) { + Consumer listener; + s_lock.lock(); + try { + listener = s_listeners.get(event.listener); + } finally { + s_lock.unlock(); + } + if (listener != null) { + try { + listener.accept(event); + } catch (Throwable throwable) { + System.err.println( + "Unhandled exception during listener callback: " + throwable.toString()); + throwable.printStackTrace(); + } + } + } + } + s_lock.lock(); + try { + if (!wasInterrupted) { + NetworkTablesJNI.destroyConnectionListenerPoller(s_poller); + } + s_poller = 0; + } finally { + s_lock.unlock(); + } + }, + "ConnectionListener"); + s_thread.setDaemon(true); + s_thread.start(); + } +} diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionListenerPoller.java b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionListenerPoller.java new file mode 100644 index 0000000000..88121cd0ad --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionListenerPoller.java @@ -0,0 +1,78 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 connection listener. This queues connection notifications. Code using the listener must + * periodically call readQueue() to read the notifications. + */ +public final class ConnectionListenerPoller implements AutoCloseable { + /** + * Construct a connection listener poller. + * + * @param inst Instance + */ + public ConnectionListenerPoller(NetworkTableInstance inst) { + m_inst = inst; + m_handle = NetworkTablesJNI.createConnectionListenerPoller(inst.getHandle()); + } + + /** + * Create a connection listener. + * + * @param immediateNotify if notification should be immediately created for existing connections + * @return Listener handle + */ + public int add(boolean immediateNotify) { + return NetworkTablesJNI.addPolledConnectionListener(m_handle, immediateNotify); + } + + /** + * Remove a connection listener. + * + * @param listener Listener handle + */ + public void remove(int listener) { + NetworkTablesJNI.removeConnectionListener(listener); + } + + /** + * Read connection notifications. + * + * @return Connection notifications since the previous call to readQueue() + */ + public ConnectionNotification[] readQueue() { + return NetworkTablesJNI.readConnectionListenerQueue(m_inst, m_handle); + } + + @Override + public synchronized void close() { + if (m_handle != 0) { + NetworkTablesJNI.destroyConnectionListenerPoller(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/ConnectionNotification.java b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java index cd031dafe5..6fe2386dfe 100644 --- a/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java +++ b/ntcore/src/main/java/edu/wpi/first/networktables/ConnectionNotification.java @@ -7,7 +7,10 @@ package edu.wpi.first.networktables; /** NetworkTables Connection notification. */ @SuppressWarnings("MemberName") public final class ConnectionNotification { - /** Listener that was triggered. */ + /** + * Handle of listener that was triggered. ConnectionListener.getHandle() or the return value of + * ConnectionListenerPoller.add() can be used to map this to a specific added listener. + */ public final int listener; /** True if event is due to connection being established. */ 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 0000000000..48d4a9c362 --- /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 bdcc6deaeb..0000000000 --- a/ntcore/src/main/java/edu/wpi/first/networktables/EntryInfo.java +++ /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. - -package edu.wpi.first.networktables; - -/** NetworkTables Entry information. */ -@SuppressWarnings("MemberName") -public final class EntryInfo { - /** Entry handle. */ - public final int entry; - - /** Entry name. */ - public final String name; - - /** Entry type. */ - public final NetworkTableType type; - - /** Entry flags. */ - public final int flags; - - /** Timestamp of last change to entry (type or value). */ - 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 856c9620a1..0000000000 --- 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. - * - *

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. - */ -public interface EntryListenerFlags { - /** - * Initial listener addition. - * - *

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. - * - *

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. - * - *

Set this flag to receive a notification when an entry is created. - */ - int kNew = 0x04; - - /** - * Entry was deleted. - * - *

Set this flag to receive a notification when an entry is deleted. - */ - int kDelete = 0x08; - - /** - * Entry's value changed. - * - *

Set this flag to receive a notification when an entry's value (or type) changes. - */ - int kUpdate = 0x10; - - /** - * Entry's flags changed. - * - *

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 a957cf73d4..0000000000 --- a/ntcore/src/main/java/edu/wpi/first/networktables/EntryNotification.java +++ /dev/null @@ -1,69 +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. */ -@SuppressWarnings("MemberName") -public final class EntryNotification { - /** Listener that was triggered. */ - public final int listener; - - /** Entry handle. */ - public final int entry; - - /** Entry name. */ - public final String name; - - /** The new value. */ - public final NetworkTableValue value; - - /** - * Update flags. For example, {@link EntryListenerFlags#kNew} if the key did not previously exist. - */ - 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 0000000000..77e7502a63 --- /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. + * + *

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/MultiSubscriber.java b/ntcore/src/main/java/edu/wpi/first/networktables/MultiSubscriber.java new file mode 100644 index 0000000000..8386c1e204 --- /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 65bfbca893..4d5e6a5330 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 @@ public interface NTSendableBuilder extends SendableBuilder { * 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 getter, Consumer 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 7837a21eba..f04ab7777f 100644 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java +++ b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTable.java @@ -11,7 +11,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.function.Consumer; /** A network table that knows its subtable path. */ public final class NetworkTable { @@ -129,6 +128,126 @@ public final class NetworkTable { 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 m_entries = new ConcurrentHashMap<>(); /** @@ -141,105 +260,14 @@ public final class NetworkTable { 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 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 +286,7 @@ public final class NetworkTable { * @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 +297,64 @@ public final class NetworkTable { * 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 getTopicInfo(int types) { + List 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 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 getTopics(int types) { + List 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 getTopics() { + return getTopics(0); } /** @@ -283,16 +366,12 @@ public final class NetworkTable { public Set getKeys(int types) { Set 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 +393,7 @@ public final class NetworkTable { public Set getSubTables() { Set 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) { @@ -325,15 +404,6 @@ public final class NetworkTable { return keys; } - /** - * 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. * @@ -375,28 +445,6 @@ public final class NetworkTable { 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); - } - - /** - * Load table values from a file. The file format used is identical to that used for - * SavePersistent / LoadPersistent. - * - * @param filename filename - * @return List of warnings (errors result in an exception instead) - * @throws PersistentException if error saving file - */ - public String[] loadEntries(String filename) throws PersistentException { - return m_inst.loadEntries(filename, m_pathWithSep); - } - @Override public boolean equals(Object other) { if (other == this) { 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 872bc89b9f..0000000000 --- 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 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 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/NetworkTableInstance.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java deleted file mode 100644 index 53ed3e03c0..0000000000 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableInstance.java +++ /dev/null @@ -1,1194 +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.datalog.DataLog; -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. - * - *

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. - * - *

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. - * - *

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 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 { - final NetworkTableEntry m_entry; - final Consumer m_consumer; - - EntryConsumer(NetworkTableEntry entry, Consumer consumer) { - m_entry = entry; - m_consumer = consumer; - } - } - - private final ReentrantLock m_entryListenerLock = new ReentrantLock(); - private final Map> m_entryListeners = new HashMap<>(); - private int m_entryListenerPoller; - private boolean m_entryListenerWaitQueue; - private final Condition m_entryListenerWaitQueueCond = m_entryListenerLock.newCondition(); - - 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 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 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 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> m_connectionListeners = - new HashMap<>(); - private int m_connectionListenerPoller; - private boolean m_connectionListenerWaitQueue; - private final Condition m_connectionListenerWaitQueueCond = - m_connectionListenerLock.newCondition(); - - 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 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 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> m_rpcCalls = new HashMap<>(); - private int m_rpcCallPoller; - private boolean m_rpcCallWaitQueue; - private final Condition m_rpcCallWaitQueueCond = m_rpcCallLock.newCondition(); - - 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 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 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); - } - - /** - * 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); - } - - private final ReentrantLock m_loggerLock = new ReentrantLock(); - private final Map> m_loggers = new HashMap<>(); - private int m_loggerPoller; - private boolean m_loggerWaitQueue; - private final Condition m_loggerWaitQueueCond = m_loggerLock.newCondition(); - - 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 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 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/NetworkTableType.java b/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableType.java index a173bb24a9..3f4df34839 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 @@ package edu.wpi.first.networktables; /** 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,86 @@ public enum NetworkTableType { 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) { + return "int"; + } else if (data instanceof Double || data instanceof Number) { + 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[]) { + 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 0c4c1ef804..0000000000 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTableValue.java +++ /dev/null @@ -1,518 +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. */ -@SuppressWarnings("PMD.MethodReturnsInternalArray") -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. - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - 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 3f2d81217e..0000000000 --- a/ntcore/src/main/java/edu/wpi/first/networktables/NetworkTablesJNI.java +++ /dev/null @@ -1,292 +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 edu.wpi.first.util.datalog.DataLog; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicBoolean; - -public final class NetworkTablesJNI { - static boolean libraryLoaded = false; - static RuntimeLoader 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(); - - 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 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 1c6159595c..0000000000 --- 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 0000000000..7beaf9ed25 --- /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 0000000000..8d277a6fd0 --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/PubSubOption.java @@ -0,0 +1,78 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 { + private static final int kPeriodic = 1; + private static final int kSendAll = 2; + private static final int kTopicsOnly = 3; + private static final int kPollStorage = 4; + private static final int kKeepDuplicates = 5; + + PubSubOption(int type, double value) { + m_type = type; + m_value = 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 (and the immediate flag is false) is 100 ms. This option + * and the immediate option override each other. + * + * @param period time between updates, in seconds + * @return option + */ + public static PubSubOption periodic(double period) { + return new PubSubOption(kPeriodic, period); + } + + /** + * If enabled, sends all value changes over the network even if only sent periodically. This + * option defaults to disabled. + * + * @param enabled True to enable, false to disable + * @return option + */ + public static PubSubOption sendAll(boolean enabled) { + return new PubSubOption(kSendAll, enabled ? 1.0 : 0.0); + } + + /** + * If enabled, no value changes are sent over the network. This option defaults to disabled. + * + * @param enabled True to enable, false to disable + * @return option + */ + public static PubSubOption topicsOnly(boolean enabled) { + return new PubSubOption(kTopicsOnly, enabled ? 1.0 : 0.0); + } + + /** + * 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(kKeepDuplicates, enabled ? 1.0 : 0.0); + } + + /** + * Polling storage for subscription. Specifies the maximum number of updates NetworkTables should + * store between calls to the subscriber's poll() function. Defaults to 1. + * + * @param depth number of entries to save for polling. + * @return option + */ + public static PubSubOption pollStorage(int depth) { + return new PubSubOption(kPollStorage, depth); + } + + final int m_type; + final double m_value; +} diff --git a/ntcore/src/main/native/cpp/networktables/RpcCall.cpp b/ntcore/src/main/java/edu/wpi/first/networktables/Publisher.java similarity index 51% rename from ntcore/src/main/native/cpp/networktables/RpcCall.cpp rename to ntcore/src/main/java/edu/wpi/first/networktables/Publisher.java index 2192a82a5f..616c7ab2a8 100644 --- a/ntcore/src/main/native/cpp/networktables/RpcCall.cpp +++ b/ntcore/src/main/java/edu/wpi/first/networktables/Publisher.java @@ -2,12 +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. -#include "networktables/RpcCall.h" +package edu.wpi.first.networktables; -#include "networktables/NetworkTableEntry.h" - -using namespace nt; - -NetworkTableEntry RpcCall::GetEntry() const { - return NetworkTableEntry{m_entry}; -} +/** 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 422ad16a5c..0000000000 --- a/ntcore/src/main/java/edu/wpi/first/networktables/RpcAnswer.java +++ /dev/null @@ -1,102 +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). */ -@SuppressWarnings("MemberName") -public final class RpcAnswer { - /** Entry handle. */ - public final int entry; - - /** Call handle. */ - public int call; - - /** Entry name. */ - public final String name; - - /** Call raw parameters. */ - public final byte[] params; - - /** Connection that called the RPC. */ - 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 790ff5fa0d..0000000000 --- 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 0000000000..08f825d09c --- /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 e0781e117b..0000000000 --- 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/Topic.java b/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java new file mode 100644 index 0000000000..cc6d6b09f7 --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/Topic.java @@ -0,0 +1,319 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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) + */ + 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 + */ + public void setProperties(String properties) { + NetworkTablesJNI.setTopicProperties(m_handle, properties); + } + + /** + * Create a new subscriber to the topic. + * + *

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. + * + *

The subscriber is only active as long as the returned object is not closed. + * + *

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. + * + *

The publisher is only active as long as the returned object is not closed. + * + *

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. + * + *

The publisher is only active as long as the returned object is not closed. + * + *

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 + */ + 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 0000000000..da44f7172e --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/TopicInfo.java @@ -0,0 +1,56 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package 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 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/TopicListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/TopicListener.java new file mode 100644 index 0000000000..72a717db9f --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/TopicListener.java @@ -0,0 +1,238 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +/** + * Topic change listener. This calls back to a callback function when a topic change matching the + * specified mask occurs. + */ +public final class TopicListener implements AutoCloseable { + /** + * Create a listener for changes on a particular topic. + * + * @param topic Topic + * @param eventMask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + public TopicListener(Topic topic, int eventMask, Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = topic.getInstance(); + s_poller = NetworkTablesJNI.createTopicListenerPoller(s_inst.getHandle()); + startThread(); + } + m_handle = NetworkTablesJNI.addPolledTopicListener(s_poller, topic.getHandle(), eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + /** + * Create a listener for topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + public TopicListener(Subscriber subscriber, int eventMask, Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = subscriber.getTopic().getInstance(); + s_poller = NetworkTablesJNI.createTopicListenerPoller(s_inst.getHandle()); + startThread(); + } + m_handle = + NetworkTablesJNI.addPolledTopicListener(s_poller, subscriber.getHandle(), eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + /** + * Create a listener for topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + public TopicListener( + MultiSubscriber subscriber, int eventMask, Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = subscriber.getInstance(); + s_poller = NetworkTablesJNI.createTopicListenerPoller(s_inst.getHandle()); + startThread(); + } + m_handle = + NetworkTablesJNI.addPolledTopicListener(s_poller, subscriber.getHandle(), eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + /** + * Create a listener for topic changes on an entry. + * + * @param entry Entry + * @param eventMask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + public TopicListener( + NetworkTableEntry entry, int eventMask, Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = entry.getInstance(); + s_poller = NetworkTablesJNI.createTopicListenerPoller(s_inst.getHandle()); + startThread(); + } + m_handle = NetworkTablesJNI.addPolledTopicListener(s_poller, entry.getHandle(), eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + /** + * Create a listener for changes to topics with names that start with any of the given prefixes. + * + * @param inst Instance + * @param prefixes Topic name string prefixes + * @param eventMask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + public TopicListener( + NetworkTableInstance inst, + String[] prefixes, + int eventMask, + Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = inst; + s_poller = NetworkTablesJNI.createTopicListenerPoller(inst.getHandle()); + startThread(); + } + m_handle = NetworkTablesJNI.addPolledTopicListener(s_poller, prefixes, eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + @Override + public synchronized void close() { + if (m_handle != 0) { + s_lock.lock(); + try { + s_listeners.remove(m_handle); + } finally { + s_lock.unlock(); + } + NetworkTablesJNI.removeTopicListener(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; + } + + private int m_handle; + + private static final ReentrantLock s_lock = new ReentrantLock(); + private static final Map> s_listeners = new HashMap<>(); + private static Thread s_thread; + private static NetworkTableInstance s_inst; + private static int s_poller; + private static boolean s_waitQueue; + private static final Condition s_waitQueueCond = s_lock.newCondition(); + + private static void startThread() { + s_thread = + new Thread( + () -> { + boolean wasInterrupted = false; + while (!Thread.interrupted()) { + try { + WPIUtilJNI.waitForObject(s_poller); + } catch (InterruptedException ex) { + s_lock.lock(); + try { + if (s_waitQueue) { + s_waitQueue = false; + s_waitQueueCond.signalAll(); + continue; + } + } finally { + s_lock.unlock(); + } + Thread.currentThread().interrupt(); + // don't try to destroy poller, as its handle is likely no longer valid + wasInterrupted = true; + break; + } + for (TopicNotification event : + NetworkTablesJNI.readTopicListenerQueue(s_inst, s_poller)) { + Consumer listener; + s_lock.lock(); + try { + listener = s_listeners.get(event.listener); + } finally { + s_lock.unlock(); + } + if (listener != null) { + try { + listener.accept(event); + } catch (Throwable throwable) { + System.err.println( + "Unhandled exception during listener callback: " + throwable.toString()); + throwable.printStackTrace(); + } + } + } + } + s_lock.lock(); + try { + if (!wasInterrupted) { + NetworkTablesJNI.destroyTopicListenerPoller(s_poller); + } + s_poller = 0; + } finally { + s_lock.unlock(); + } + }, + "TopicListener"); + s_thread.setDaemon(true); + s_thread.start(); + } +} diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerFlags.java b/ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerFlags.java new file mode 100644 index 0000000000..2592fc5de4 --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerFlags.java @@ -0,0 +1,47 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 topic listeners. + * + *

The flags are a bitmask and must be OR'ed together to indicate the combination of events + * desired to be received. + * + *

The constants kPublish, kUnpublish, and kProperties represent different events that can occur + * to topics. + */ +public enum TopicListenerFlags { + ; // no enum values + + /** + * Initial listener addition. + * + *

Set this flag to receive immediate notification of topics matching the flag criteria + * (generally only useful when combined with kPublish). + */ + public static final int kImmediate = 0x01; + + /** + * Newly published topic. + * + *

Set this flag to receive a notification when a topic is initially published. + */ + public static final int kPublish = 0x02; + + /** + * Topic has no more publishers. + * + *

Set this flag to receive a notification when a topic has no more publishers. + */ + public static final int kUnpublish = 0x04; + + /** + * Topic's properties changed. + * + *

Set this flag to receive a notification when an topic's properties change. + */ + public static final int kProperties = 0x08; +} diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerPoller.java b/ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerPoller.java new file mode 100644 index 0000000000..9a0e2f9c7d --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/TopicListenerPoller.java @@ -0,0 +1,124 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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; + +/** + * Topic change listener. This queues topic change events matching the specified mask. Code using + * the listener must periodically call readQueue() to read the events. + */ +public final class TopicListenerPoller implements AutoCloseable { + /** + * Construct a topic listener poller. + * + * @param inst Instance + */ + public TopicListenerPoller(NetworkTableInstance inst) { + m_inst = inst; + m_handle = NetworkTablesJNI.createTopicListenerPoller(inst.getHandle()); + } + + /** + * Start listening to changes to a particular topic. + * + * @param topic Topic + * @param eventMask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + public int add(Topic topic, int eventMask) { + return NetworkTablesJNI.addPolledTopicListener(m_handle, topic.getHandle(), eventMask); + } + + /** + * Start listening to topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + public int add(Subscriber subscriber, int eventMask) { + return NetworkTablesJNI.addPolledTopicListener(m_handle, subscriber.getHandle(), eventMask); + } + + /** + * Start listening to topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + public int add(MultiSubscriber subscriber, int eventMask) { + return NetworkTablesJNI.addPolledTopicListener(m_handle, subscriber.getHandle(), eventMask); + } + + /** + * Start listening to topic changes on an entry. + * + * @param entry Entry + * @param eventMask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + public int add(NetworkTableEntry entry, int eventMask) { + return NetworkTablesJNI.addPolledTopicListener(m_handle, entry.getHandle(), eventMask); + } + + /** + * Start listening to topic changes for topics with names that start with any of the given + * prefixes. + * + * @param prefixes Topic name string prefixes + * @param eventMask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + public int add(String[] prefixes, int eventMask) { + return NetworkTablesJNI.addPolledTopicListener(m_handle, prefixes, eventMask); + } + + /** + * Remove a listener. + * + * @param listener Listener handle + */ + public void remove(int listener) { + NetworkTablesJNI.removeTopicListener(listener); + } + + /** + * Read topic notifications. + * + * @return Topic notifications since the previous call to readQueue() + */ + public TopicNotification[] readQueue() { + return NetworkTablesJNI.readTopicListenerQueue(m_inst, m_handle); + } + + @Override + public synchronized void close() { + if (m_handle != 0) { + NetworkTablesJNI.destroyTopicListenerPoller(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/TopicNotification.java b/ntcore/src/main/java/edu/wpi/first/networktables/TopicNotification.java new file mode 100644 index 0000000000..7f0de52ac3 --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/TopicNotification.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 topic notification. */ +@SuppressWarnings("MemberName") +public final class TopicNotification { + /** + * Handle of listener that was triggered. TopicListener.getHandle() or the return value of + * TopicListenerPoller.add() can be used to map this to a specific added listener. + */ + public final int listener; + + /** Topic information. */ + public final TopicInfo info; + + /** + * Update flags. For example, {@link TopicListenerFlags#kPublish} if the topic was not previously + * published. + */ + public final int flags; + + /** + * Constructor. This should generally only be used internally to NetworkTables. + * + * @param listener Listener that was triggered + * @param info Topic information + * @param flags Update flags + */ + public TopicNotification(int listener, TopicInfo info, int flags) { + this.listener = listener; + this.info = info; + this.flags = flags; + } +} diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ValueListener.java b/ntcore/src/main/java/edu/wpi/first/networktables/ValueListener.java new file mode 100644 index 0000000000..9bded83cbb --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/ValueListener.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 edu.wpi.first.util.WPIUtilJNI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +/** + * Value change listener. This calls back to a callback function when a value change matching the + * specified mask occurs. + */ +public final class ValueListener implements AutoCloseable { + /** + * Create a listener for value changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of ValueListenerFlags values + * @param listener Listener function + */ + public ValueListener(Subscriber subscriber, int eventMask, Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = subscriber.getTopic().getInstance(); + s_poller = NetworkTablesJNI.createValueListenerPoller(s_inst.getHandle()); + startThread(); + } + m_handle = + NetworkTablesJNI.addPolledValueListener(s_poller, subscriber.getHandle(), eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + /** + * Create a listener for value changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of ValueListenerFlags values + * @param listener Listener function + */ + public ValueListener( + MultiSubscriber subscriber, int eventMask, Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = subscriber.getInstance(); + s_poller = NetworkTablesJNI.createValueListenerPoller(s_inst.getHandle()); + startThread(); + } + m_handle = + NetworkTablesJNI.addPolledValueListener(s_poller, subscriber.getHandle(), eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + /** + * Create a listener for value changes on an entry. + * + * @param entry Entry + * @param eventMask Bitmask of ValueListenerFlags values + * @param listener Listener function + */ + public ValueListener( + NetworkTableEntry entry, int eventMask, Consumer listener) { + s_lock.lock(); + try { + if (s_poller == 0) { + s_inst = entry.getInstance(); + s_poller = NetworkTablesJNI.createValueListenerPoller(s_inst.getHandle()); + startThread(); + } + m_handle = NetworkTablesJNI.addPolledValueListener(s_poller, entry.getHandle(), eventMask); + s_listeners.put(m_handle, listener); + } finally { + s_lock.unlock(); + } + } + + @Override + public synchronized void close() { + if (m_handle != 0) { + s_lock.lock(); + try { + s_listeners.remove(m_handle); + } finally { + s_lock.unlock(); + } + NetworkTablesJNI.removeValueListener(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; + } + + private int m_handle; + + private static final ReentrantLock s_lock = new ReentrantLock(); + private static final Map> s_listeners = new HashMap<>(); + private static Thread s_thread; + private static NetworkTableInstance s_inst; + private static int s_poller; + private static boolean s_waitQueue; + private static final Condition s_waitQueueCond = s_lock.newCondition(); + + private static void startThread() { + s_thread = + new Thread( + () -> { + boolean wasInterrupted = false; + while (!Thread.interrupted()) { + try { + WPIUtilJNI.waitForObject(s_poller); + } catch (InterruptedException ex) { + s_lock.lock(); + try { + if (s_waitQueue) { + s_waitQueue = false; + s_waitQueueCond.signalAll(); + continue; + } + } finally { + s_lock.unlock(); + } + Thread.currentThread().interrupt(); + // don't try to destroy poller, as its handle is likely no longer valid + wasInterrupted = true; + break; + } + for (ValueNotification event : + NetworkTablesJNI.readValueListenerQueue(s_inst, s_poller)) { + Consumer listener; + s_lock.lock(); + try { + listener = s_listeners.get(event.listener); + } finally { + s_lock.unlock(); + } + if (listener != null) { + try { + listener.accept(event); + } catch (Throwable throwable) { + System.err.println( + "Unhandled exception during listener callback: " + throwable.toString()); + throwable.printStackTrace(); + } + } + } + } + s_lock.lock(); + try { + if (!wasInterrupted) { + NetworkTablesJNI.destroyValueListenerPoller(s_poller); + } + s_poller = 0; + } finally { + s_lock.unlock(); + } + }, + "ValueListener"); + s_thread.setDaemon(true); + s_thread.start(); + } +} diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerFlags.java b/ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerFlags.java new file mode 100644 index 0000000000..9a303d6d75 --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerFlags.java @@ -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. + +package edu.wpi.first.networktables; + +/** + * Flag values for use with value listeners. + * + *

The flags are a bitmask and must be OR'ed together to indicate the combination of events + * desired to be received. + * + *

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 enum ValueListenerFlags { + ; // no enum values + + /** + * Initial listener addition. + * + *

Set this flag to receive immediate notification of the current value. + */ + public static final int kImmediate = 0x01; + + /** + * 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. + */ + public static final int kLocal = 0x02; +} diff --git a/ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerPoller.java b/ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerPoller.java new file mode 100644 index 0000000000..dc7142c83e --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/ValueListenerPoller.java @@ -0,0 +1,101 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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; + +/** + * Value change listener. This queues value change events matching the specified mask. Code using + * the listener must periodically call readQueue() to read the events. + */ +public final class ValueListenerPoller implements AutoCloseable { + /** + * Construct a value listener poller. + * + * @param inst Instance + */ + public ValueListenerPoller(NetworkTableInstance inst) { + m_inst = inst; + m_handle = NetworkTablesJNI.createValueListenerPoller(inst.getHandle()); + } + + /** + * Start listening to value changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of ValueListenerFlags values + * @return Listener handle + */ + public int add(Subscriber subscriber, int eventMask) { + return NetworkTablesJNI.addPolledValueListener(m_handle, subscriber.getHandle(), eventMask); + } + + /** + * Start listening to value changes on a subscriber. + * + * @param subscriber Subscriber + * @param eventMask Bitmask of ValueListenerFlags values + * @return Listener handle + */ + public int add(MultiSubscriber subscriber, int eventMask) { + return NetworkTablesJNI.addPolledValueListener(m_handle, subscriber.getHandle(), eventMask); + } + + /** + * Start listening to value changes on an entry. + * + * @param entry Entry + * @param eventMask Bitmask of ValueListenerFlags values + * @return Listener handle + */ + public int add(NetworkTableEntry entry, int eventMask) { + return NetworkTablesJNI.addPolledValueListener(m_handle, entry.getHandle(), eventMask); + } + + /** + * Remove a listener. + * + * @param listener Listener handle + */ + public void remove(int listener) { + NetworkTablesJNI.removeValueListener(listener); + } + + /** + * Read value notifications. + * + * @return Value notifications since the previous call to readQueue() + */ + public ValueNotification[] readQueue() { + return NetworkTablesJNI.readValueListenerQueue(m_inst, m_handle); + } + + @Override + public synchronized void close() { + if (m_handle != 0) { + NetworkTablesJNI.destroyValueListenerPoller(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/ValueNotification.java b/ntcore/src/main/java/edu/wpi/first/networktables/ValueNotification.java new file mode 100644 index 0000000000..1a57204abd --- /dev/null +++ b/ntcore/src/main/java/edu/wpi/first/networktables/ValueNotification.java @@ -0,0 +1,73 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.networktables; + +/** NetworkTables value notification. */ +@SuppressWarnings("MemberName") +public final class ValueNotification { + /** + * Handle of listener that was triggered. ValueListener.getHandle() or the return value of + * ValueListenerPoller.add() can be used to map this to a specific added listener. + */ + public final int listener; + + /** 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; + + /** Update flags. */ + public final int flags; + + /** + * Constructor. This should generally only be used internally to NetworkTables. + * + * @param inst Instance + * @param listener Listener that was triggered + * @param topic Topic handle + * @param subentry Subscriber/entry handle + * @param value The new value + * @param flags Update flags + */ + public ValueNotification( + NetworkTableInstance inst, + int listener, + int topic, + int subentry, + NetworkTableValue value, + int flags) { + this.m_inst = inst; + this.listener = listener; + this.topic = topic; + this.subentry = subentry; + this.value = value; + this.flags = flags; + } + + /* Network table instance. */ + private final NetworkTableInstance m_inst; + + /* Cached topic object. */ + Topic m_topicObject; + + /** + * 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 0000000000..fffcb7d17e --- /dev/null +++ b/ntcore/src/main/native/cpp/ConnectionList.cpp @@ -0,0 +1,338 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "HandleMap.h" +#include "ntcore_cpp.h" + +using namespace nt; + +namespace { + +struct PollerData { + static constexpr auto kType = Handle::kConnectionListenerPoller; + + explicit PollerData(NT_ConnectionListenerPoller handle) : handle{handle} {} + + wpi::SignalObject handle; + std::vector queue; +}; + +struct ListenerData { + static constexpr auto kType = Handle::kConnectionListener; + + ListenerData(NT_ConnectionListener handle, PollerData* poller) + : handle{handle}, poller{poller} {} + + wpi::SignalObject handle; + PollerData* poller; +}; + +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; +}; + +class ListenerThread final : public wpi::SafeThreadEvent { + public: + explicit ListenerThread(NT_ConnectionListenerPoller poller) + : m_poller{poller} {} + + void Main() final; + + NT_ConnectionListenerPoller m_poller; + wpi::DenseMap> + m_callbacks; +}; + +class CLImpl { + public: + explicit CLImpl(int inst) : m_inst{inst} {} + + int m_inst; + + // shared with user (must be atomic or mutex-protected) + std::atomic_bool m_connected{false}; + wpi::UidVector, 8> m_connections; + + HandleMap m_pollers; + HandleMap m_listeners; + HandleMap m_dataloggers; + + wpi::SafeThreadOwner m_listenerThread; + + NT_ConnectionListener AddListener( + std::function callback, + bool immediateNotify); + PollerData* CreateListenerPoller() { return m_pollers.Add(m_inst); } + void DestroyListenerPoller(NT_ConnectionListenerPoller pollerHandle); + NT_ConnectionListener AddPolledListener( + NT_ConnectionListenerPoller pollerHandle, bool immediateNotify); + void RemoveListener(NT_ConnectionListener listenerHandle); +}; + +} // namespace + +void ListenerThread::Main() { + while (m_active) { + WPI_Handle signaledBuf[2]; + auto signaled = + wpi::WaitForObjects({m_poller, m_stopEvent.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::ReadConnectionListenerQueue(m_poller); + if (events.empty()) { + continue; + } + 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(); + } + } + } +} + +NT_ConnectionListener CLImpl::AddListener( + std::function callback, + bool immediateNotify) { + if (!m_listenerThread) { + m_listenerThread.Start(CreateListenerPoller()->handle); + } + if (auto thr = m_listenerThread.GetThread()) { + auto listener = AddPolledListener(thr->m_poller, immediateNotify); + if (listener) { + thr->m_callbacks.try_emplace(listener, std::move(callback)); + } + return listener; + } else { + return {}; + } +} + +void CLImpl::DestroyListenerPoller(NT_ConnectionListenerPoller pollerHandle) { + if (auto poller = m_pollers.Remove(pollerHandle)) { + // ensure all listeners that use this poller are removed + wpi::SmallVector toRemove; + for (auto&& listener : m_listeners) { + if (listener->poller == poller.get()) { + toRemove.emplace_back(listener->handle); + } + } + for (auto handle : toRemove) { + RemoveListener(handle); + } + } +} + +NT_ConnectionListener CLImpl::AddPolledListener( + NT_ConnectionListenerPoller pollerHandle, bool immediateNotify) { + auto poller = m_pollers.Get(pollerHandle); + if (!poller) { + return {}; + } + + auto listener = m_listeners.Add(m_inst, poller); + if (immediateNotify && !m_connections.empty()) { + for (auto&& conn : m_connections) { + listener->poller->queue.emplace_back(listener->handle.GetHandle(), true, + *conn); + } + listener->poller->handle.Set(); + listener->handle.Set(); + } + return listener->handle; +} + +void CLImpl::RemoveListener(NT_ConnectionListener listenerHandle) { + if (auto listener = m_listeners.Remove(listenerHandle)) { + if (auto thr = m_listenerThread.GetThread()) { + if (thr->m_poller == listener->poller->handle) { + thr->m_callbacks.erase(listenerHandle); + } + } + } +} + +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(info.remote_port)); + os << ",\"protocol_version\":"; + s.dump_integer(static_cast(info.protocol_version)); + os << "}"; + os.flush(); + return str; +} + +class ConnectionList::Impl : public CLImpl { + public: + explicit Impl(int inst) : CLImpl{inst} {} +}; + +ConnectionList::ConnectionList(int inst) + : m_impl{std::make_unique(inst)} {} + +ConnectionList::~ConnectionList() = default; + +int ConnectionList::AddConnection(const ConnectionInfo& info) { + std::scoped_lock lock{m_mutex}; + m_impl->m_connected = true; + for (auto&& listener : m_impl->m_listeners) { + listener->poller->queue.emplace_back(listener->handle.GetHandle(), true, + info); + listener->poller->handle.Set(); + listener->handle.Set(); + } + if (!m_impl->m_dataloggers.empty()) { + auto now = Now(); + for (auto&& datalogger : m_impl->m_dataloggers) { + datalogger->entry.Append(ConnInfoToJson(true, info), now); + } + } + return m_impl->m_connections.emplace_back(info); +} + +void ConnectionList::RemoveConnection(int handle) { + std::scoped_lock lock{m_mutex}; + auto val = m_impl->m_connections.erase(handle); + if (m_impl->m_connections.empty()) { + m_impl->m_connected = false; + } + if (val) { + for (auto&& listener : m_impl->m_listeners) { + listener->poller->queue.emplace_back(listener->handle.GetHandle(), false, + *val); + listener->poller->handle.Set(); + listener->handle.Set(); + } + if (!m_impl->m_dataloggers.empty()) { + auto now = Now(); + for (auto&& datalogger : m_impl->m_dataloggers) { + datalogger->entry.Append(ConnInfoToJson(false, *val), now); + } + } + } +} + +void ConnectionList::ClearConnections() { + std::scoped_lock lock{m_mutex}; + m_impl->m_connected = false; + for (auto&& conn : m_impl->m_connections) { + for (auto&& listener : m_impl->m_listeners) { + listener->poller->queue.emplace_back(listener->handle.GetHandle(), false, + *conn); + listener->poller->handle.Set(); + listener->handle.Set(); + } + } + m_impl->m_connections.clear(); +} + +std::vector ConnectionList::GetConnections() const { + std::scoped_lock lock{m_mutex}; + std::vector info; + info.reserve(m_impl->m_connections.size()); + for (auto&& conn : m_impl->m_connections) { + info.emplace_back(*conn); + } + return info; +} + +bool ConnectionList::IsConnected() const { + return m_impl->m_connected; +} + +NT_ConnectionListener ConnectionList::AddListener( + std::function callback, + bool immediateNotify) { + std::scoped_lock lock{m_mutex}; + return m_impl->AddListener(std::move(callback), immediateNotify); +} + +NT_ConnectionListenerPoller ConnectionList::CreateListenerPoller() { + std::scoped_lock lock{m_mutex}; + return m_impl->CreateListenerPoller()->handle; +} + +void ConnectionList::DestroyListenerPoller( + NT_ConnectionListenerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + m_impl->DestroyListenerPoller(pollerHandle); +} + +NT_ConnectionListener ConnectionList::AddPolledListener( + NT_ConnectionListenerPoller pollerHandle, bool immediateNotify) { + std::scoped_lock lock{m_mutex}; + return m_impl->AddPolledListener(pollerHandle, immediateNotify); +} + +std::vector ConnectionList::ReadListenerQueue( + NT_ConnectionListenerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + if (auto poller = m_impl->m_pollers.Get(pollerHandle)) { + std::vector rv; + rv.swap(poller->queue); + return rv; + } else { + return {}; + } +} + +void ConnectionList::RemoveListener(NT_ConnectionListener listenerHandle) { + std::scoped_lock lock{m_mutex}; + m_impl->RemoveListener(listenerHandle); +} + +NT_ConnectionDataLogger ConnectionList::StartDataLog(wpi::log::DataLog& log, + std::string_view name) { + std::scoped_lock lock{m_mutex}; + auto now = Now(); + auto datalogger = m_impl->m_dataloggers.Add(m_impl->m_inst, log, name, now); + for (auto&& conn : m_impl->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_impl->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 0000000000..ab52dabe68 --- /dev/null +++ b/ntcore/src/main/native/cpp/ConnectionList.h @@ -0,0 +1,56 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include +#include +#include + +#include + +#include "IConnectionList.h" +#include "ntcore_cpp.h" + +namespace nt { + +class ConnectionList final : public IConnectionList { + public: + explicit ConnectionList(int inst); + ~ConnectionList() final; + + // IConnectionList interface + int AddConnection(const ConnectionInfo& info) final; + void RemoveConnection(int handle) final; + void ClearConnections() final; + + // user-facing functions + std::vector GetConnections() const final; + bool IsConnected() const final; + + NT_ConnectionListener AddListener( + std::function callback, + bool immediateNotify); + + NT_ConnectionListenerPoller CreateListenerPoller(); + void DestroyListenerPoller(NT_ConnectionListenerPoller pollerHandle); + NT_ConnectionListener AddPolledListener( + NT_ConnectionListenerPoller pollerHandle, bool immediateNotify); + std::vector ReadListenerQueue( + NT_ConnectionListenerPoller pollerHandle); + void RemoveListener(NT_ConnectionListener listenerHandle); + + NT_ConnectionDataLogger StartDataLog(wpi::log::DataLog& log, + std::string_view name); + void StopDataLog(NT_ConnectionDataLogger logger); + + private: + class Impl; + std::unique_ptr m_impl; + + mutable wpi::mutex m_mutex; +}; + +} // 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 eb57ef7892..0000000000 --- a/ntcore/src/main/native/cpp/ConnectionNotifier.cpp +++ /dev/null @@ -1,32 +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 callback) { - return DoAdd(callback); -} - -unsigned int ConnectionNotifier::AddPolled(unsigned int poller_uid) { - return DoAdd(poller_uid); -} - -void ConnectionNotifier::Remove(unsigned int uid) { - CallbackManager::Remove(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 8ea2b78bd5..0000000000 --- a/ntcore/src/main/native/cpp/ConnectionNotifier.h +++ /dev/null @@ -1,77 +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 - -#include - -#include "Handle.h" -#include "IConnectionNotifier.h" -#include "ntcore_cpp.h" - -namespace nt { - -namespace impl { - -class ConnectionNotifierThread - : public wpi::CallbackThread { - public: - ConnectionNotifierThread(std::function on_start, - std::function 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 callback, - const ConnectionNotification& data) { - callback(data); - } - - int m_inst; -}; - -} // namespace impl - -class ConnectionNotifier - : public IConnectionNotifier, - public wpi::CallbackManager { - friend class ConnectionNotifierTest; - friend class wpi::CallbackManager; - - public: - explicit ConnectionNotifier(int inst); - - void Start(); - - unsigned int Add(std::function - callback) override; - unsigned int AddPolled(unsigned int poller_uid) override; - - void Remove(unsigned int 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 a31b73f045..0000000000 --- a/ntcore/src/main/native/cpp/Dispatcher.cpp +++ /dev/null @@ -1,765 +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 -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "IConnectionNotifier.h" -#include "IStorage.h" -#include "Log.h" -#include "NetworkConnection.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(info.remote_port)); - os << ",\"protocol_version\":"; - s.dump_integer(static_cast(info.protocol_version)); - os << "}"; - os.flush(); - return str; -} - -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(new wpi::TCPAcceptor( - static_cast(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 { - return wpi::TCPConnector::connect(server_name_copy.c_str(), - static_cast(port), m_logger, 1); - }); -} - -void Dispatcher::SetServer( - wpi::span> servers) { - wpi::SmallVector, 16> servers_copy; - for (const auto& server : servers) { - servers_copy.emplace_back(std::string{wpi::trim(server.first)}, - static_cast(server.second)); - } - - SetConnector([=]() -> std::unique_ptr { - wpi::SmallVector, 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 servers[5]; - - // 10.te.am.2 - auto fixed = fmt::format("10.{}.{}.2", static_cast(team / 100), - static_cast(team % 100)); - servers[0] = {fixed, port}; - - // 172.22.11.2 - servers[1] = {"172.22.11.2", port}; - - // roboRIO--FRC.local - auto mdns = fmt::format("roboRIO-{}-FRC.local", team); - servers[2] = {mdns, port}; - - // roboRIO--FRC.lan - auto mdns_lan = fmt::format("roboRIO-{}-FRC.lan", team); - servers[3] = {mdns_lan, port}; - - // roboRIO--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 { - return wpi::TCPConnector::connect(server_name_copy.c_str(), - static_cast(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(); - - { - std::scoped_lock lock(m_user_mutex); - for (auto&& datalog : m_dataloggers) { - m_notifier.Remove(datalog.notifier); - } - } -} - -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 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> 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(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 DispatcherBase::GetConnections() const { - std::vector 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 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; -} - -unsigned int DispatcherBase::StartDataLog(wpi::log::DataLog& log, - std::string_view name) { - std::scoped_lock lock(m_user_mutex); - auto now = nt::Now(); - unsigned int uid = m_dataloggers.emplace_back(log, name, now); - m_dataloggers[uid].notifier = - m_notifier.Add([this, uid](const ConnectionNotification& n) { - std::scoped_lock lock(m_user_mutex); - if (uid < m_dataloggers.size() && m_dataloggers[uid].entry) { - m_dataloggers[uid].entry.Append(ConnInfoToJson(n.connected, n.conn), - nt::Now()); - } - }); - for (auto& conn : m_connections) { - if (conn->state() != NetworkConnection::kActive) { - continue; - } - m_dataloggers[uid].entry.Append(ConnInfoToJson(true, conn->info()), now); - } - return uid; -} - -void DispatcherBase::StopDataLog(unsigned int logger) { - std::scoped_lock lock(m_user_mutex); - m_notifier.Remove(m_dataloggers.erase(logger).notifier); -} - -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 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 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( - ++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(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( - ++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(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()> get_msg, - std::function>)> 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> 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", - static_cast(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()> get_msg, - std::function>)> 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> 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", - static_cast(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()); - } - } - - 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 546bf06260..0000000000 --- a/ntcore/src/main/native/cpp/Dispatcher.h +++ /dev/null @@ -1,168 +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 -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#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()>; - - 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 acceptor); - void StartClient(); - void Stop(); - void SetUpdateRate(double interval); - void SetIdentity(std::string_view name); - void Flush(); - std::vector GetConnections() const; - bool IsConnected() const; - - unsigned int AddListener( - std::function callback, - bool immediate_notify) const; - unsigned int AddPolledListener(unsigned int poller_uid, - bool immediate_notify) const; - - unsigned int StartDataLog(wpi::log::DataLog& log, std::string_view name); - void StopDataLog(unsigned int logger); - - 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()> get_msg, - std::function>)> send_msgs); - bool ServerHandshake( - NetworkConnection& conn, - std::function()> get_msg, - std::function>)> send_msgs); - - void ClientReconnect(unsigned int proto_rev = 0x0300); - - void QueueOutgoing(std::shared_ptr 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 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> 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; - - struct DataLogger { - DataLogger() = default; - DataLogger(wpi::log::DataLog& log, std::string_view name, int64_t time) - : entry{log, name, - "{\"schema\":\"NTConnectionInfo\",\"source\":\"NT\"}", "json", - time} {} - - explicit operator bool() const { return static_cast(entry); } - - wpi::log::StringLogEntry entry; - unsigned int notifier = 0; - }; - wpi::UidVector m_dataloggers; - - 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> 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 ebc51c7e2c..0000000000 --- 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 -#include -#include -#include -#include - -#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 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( - 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 73fc3d382a..0000000000 --- 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 - -#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 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 6f251a3df5..0000000000 --- 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 - -#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 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 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, - 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 bbe2172b2e..0000000000 --- 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 -#include -#include -#include -#include - -#include - -#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> { - EntryListenerData() = default; - EntryListenerData( - std::function callback_, - std::string_view prefix_, unsigned int flags_) - : CallbackListenerData(callback_), prefix(prefix_), flags(flags_) {} - EntryListenerData( - std::function 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 { - public: - EntryNotifierThread(std::function on_start, - std::function 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 callback, - const EntryNotification& data) { - callback(data); - } - - int m_inst; -}; - -} // namespace impl - -class EntryNotifier - : public IEntryNotifier, - public wpi::CallbackManager { - friend class EntryNotifierTest; - friend class wpi::CallbackManager; - - public: - explicit EntryNotifier(int inst, wpi::Logger& logger); - - void Start(); - - bool local_notifiers() const override; - - unsigned int Add(std::function callback, - std::string_view prefix, unsigned int flags) override; - unsigned int Add(std::function 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, 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 47e26e6065..ccd6f613b8 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 @@ -27,11 +26,19 @@ class Handle { kInstance, kLogger, kLoggerPoller, - kRpcCall, - kRpcCallPoller, kDataLogger, - kConnectionDataLogger + kConnectionDataLogger, + kMultiSubscriber, + kTopic, + kTopicListener, + kTopicListenerPoller, + kSubscriber, + kPublisher, + kValueListener, + kValueListenerPoller, + kTypeMax }; + static_assert(kTypeMax <= wpi::kHandleTypeHALBase); enum { kIndexMax = 0xfffff }; explicit Handle(NT_Handle handle) : m_handle(handle) {} @@ -48,7 +55,9 @@ class Handle { (index & 0xfffff); } - int GetIndex() const { return static_cast(m_handle) & 0xfffff; } + unsigned int GetIndex() const { + return static_cast(m_handle) & 0xfffff; + } Type GetType() const { return static_cast((static_cast(m_handle) >> 24) & 0x7f); } @@ -62,5 +71,3 @@ class Handle { }; } // 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 0000000000..03e73f9b15 --- /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 +#include + +#include + +#include "Handle.h" + +namespace nt { + +// Utility wrapper class for our UidVectors +template +class HandleMap : public wpi::UidVector, Size> { + public: + template + T* Add(int inst, Args&&... args) { + auto i = this->emplace_back(); + auto& it = (*this)[i]; + it = std::make_unique(Handle(inst, i, T::kType), + std::forward(args)...); + return it.get(); + } + + std::unique_ptr 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 0000000000..a4a59e3c60 --- /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 + +#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 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 33e6f3b1fe..0000000000 --- a/ntcore/src/main/native/cpp/IConnectionNotifier.h +++ /dev/null @@ -1,30 +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 - -#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 callback) = 0; - virtual unsigned int AddPolled(unsigned int poller_uid) = 0; - virtual void Remove(unsigned int 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 4cf8a5fc27..0000000000 --- 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 - -#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 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 b3f91b2efa..0000000000 --- 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 -#include -#include - -#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 callback, - std::string_view prefix, unsigned int flags) = 0; - virtual unsigned int Add( - std::function 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, unsigned int flags, - unsigned int only_listener = UINT_MAX) = 0; -}; - -} // namespace nt - -#endif // NTCORE_IENTRYNOTIFIER_H_ diff --git a/ntcore/src/main/native/cpp/INetworkClient.h b/ntcore/src/main/native/cpp/INetworkClient.h new file mode 100644 index 0000000000..ac90f7baf5 --- /dev/null +++ b/ntcore/src/main/native/cpp/INetworkClient.h @@ -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. + +#pragma once + +#include +#include +#include +#include + +#include + +#include "ntcore_cpp.h" + +namespace nt { + +class INetworkClient { + public: + virtual ~INetworkClient() = default; + + virtual void SetServers( + wpi::span> 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 94e9bb16f9..0000000000 --- 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 - -#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 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 aa16084ed1..0000000000 --- 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 -#include - -#include "Message.h" -#include "ntcore_cpp.h" - -namespace nt { - -class IRpcServer { - public: - using SendResponseFunc = std::function; - - 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 795d032970..0000000000 --- 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 -#include -#include -#include - -#include - -#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 msg, - INetworkConnection* conn, - std::weak_ptr conn_weak) = 0; - virtual void GetInitialAssignments( - INetworkConnection& conn, - std::vector>* msgs) = 0; - virtual void ApplyInitialAssignments( - INetworkConnection& conn, wpi::span> msgs, - bool new_server, std::vector>* 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 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 8d59a0b5b5..9b165daabb 100644 --- a/ntcore/src/main/native/cpp/InstanceImpl.cpp +++ b/ntcore/src/main/native/cpp/InstanceImpl.cpp @@ -16,19 +16,12 @@ 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) { + connectionList(inst), + localStorage(inst, logger), + m_inst{inst} { logger.set_min_level(logger_impl.GetMinLevel()); } -InstanceImpl::~InstanceImpl() { - logger.SetLogger(nullptr); -} - InstanceImpl* InstanceImpl::GetDefault() { return Get(GetDefaultIndex()); } @@ -83,7 +76,90 @@ void InstanceImpl::Destroy(int inst) { 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( + 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; +} + +void InstanceImpl::StopServer() { + std::scoped_lock lock{m_mutex}; + if ((networkMode & NT_NET_MODE_SERVER) == 0) { + return; + } + m_networkServer.reset(); + networkMode = NT_NET_MODE_NONE; +} + +void InstanceImpl::StartClient3() { + std::scoped_lock lock{m_mutex}; + if (networkMode != NT_NET_MODE_NONE) { + return; + } + m_networkClient = std::make_shared( + m_inst, m_identity, localStorage, connectionList, logger); + networkMode = NT_NET_MODE_CLIENT3; +} + +void InstanceImpl::StartClient4() { + std::scoped_lock lock{m_mutex}; + if (networkMode != NT_NET_MODE_NONE) { + return; + } + m_networkClient = std::make_shared( + m_inst, m_identity, localStorage, connectionList, logger); + networkMode = NT_NET_MODE_CLIENT4; +} + +void InstanceImpl::StopClient() { + std::scoped_lock lock{m_mutex}; + if ((networkMode & (NT_NET_MODE_CLIENT3 | NT_NET_MODE_CLIENT4)) == 0) { + return; + } + m_networkClient.reset(); + networkMode = NT_NET_MODE_NONE; +} + +void InstanceImpl::SetIdentity(std::string_view identity) { + std::scoped_lock lock{m_mutex}; + m_identity = identity; +} + +std::shared_ptr InstanceImpl::GetServer() { + std::scoped_lock lock{m_mutex}; + return m_networkServer; +} + +std::shared_ptr InstanceImpl::GetClient() { + std::scoped_lock lock{m_mutex}; + return m_networkClient; } diff --git a/ntcore/src/main/native/cpp/InstanceImpl.h b/ntcore/src/main/native/cpp/InstanceImpl.h index 22d94f5d56..126c36a409 100644 --- a/ntcore/src/main/native/cpp/InstanceImpl.h +++ b/ntcore/src/main/native/cpp/InstanceImpl.h @@ -2,45 +2,62 @@ // Open Source 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 #include +#include +#include +#include #include -#include "ConnectionNotifier.h" -#include "Dispatcher.h" -#include "DsClient.h" -#include "EntryNotifier.h" +#include "ConnectionList.h" +#include "Handle.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(); + void StartClient4(); + void StopClient(); + void SetIdentity(std::string_view identity); + + std::shared_ptr GetServer(); + std::shared_ptr GetClient(); + 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 networkMode{NT_NET_MODE_NONE}; private: static int AllocImpl(); @@ -49,8 +66,12 @@ class InstanceImpl { static constexpr int kNumInstances = 16; static std::atomic s_instances[kNumInstances]; static wpi::mutex s_mutex; + + wpi::mutex m_mutex; + std::string m_identity; + std::shared_ptr m_networkServer; + std::shared_ptr m_networkClient; + int m_inst; }; } // namespace nt - -#endif // NTCORE_INSTANCEIMPL_H_ diff --git a/ntcore/src/main/native/cpp/LocalStorage.cpp b/ntcore/src/main/native/cpp/LocalStorage.cpp new file mode 100644 index 0000000000..c2479fe0d7 --- /dev/null +++ b/ntcore/src/main/native/cpp/LocalStorage.cpp @@ -0,0 +1,2489 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Handle.h" +#include "HandleMap.h" +#include "Log.h" +#include "PubSubOptions.h" +#include "Types_internal.h" +#include "networktables/NetworkTableValue.h" +#include "ntcore_c.h" + +using namespace nt; + +namespace { + +// Utility wrapper for making a set-like vector +template +class VectorSet : public std::vector { + public: + void Add(T value) { this->push_back(value); } + void Remove(T value) { + this->erase(std::remove(this->begin(), this->end(), value), this->end()); + } +}; + +struct EntryData; +struct PublisherData; +struct SubscriberData; +struct MultiSubscriberData; +struct TopicListenerPollerData; +struct TopicListenerData; +struct ValueListenerPollerData; +struct ValueListenerData; + +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} {} + + bool Exists() const { return onNetwork || !localPublishers.empty(); } + + TopicInfo GetTopicInfo() const; + + // invariants + wpi::SignalObject handle; + std::string name; + + Value lastValue; // also stores timestamp + 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(); + + bool onNetwork{false}; // true if there are any remote publishers + + wpi::SmallVector datalogs; + NT_Type datalogType{NT_UNASSIGNED}; + + VectorSet localPublishers; + VectorSet localSubscribers; + VectorSet multiSubscribers; + VectorSet entries; + VectorSet listeners; +}; + +struct PubSubConfig : public PubSubOptions { + PubSubConfig() = default; + PubSubConfig(NT_Type type, std::string_view typeStr, + wpi::span options) + : PubSubOptions{options}, type{type}, typeStr{typeStr} {} + + 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 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.pollStorageSize} {} + + void UpdateActive(); + + // invariants + wpi::SignalObject handle; + TopicData* topic; + PubSubConfig config; + + // whether or not the subscriber should actually receive values + bool active{false}; + + // polling storage + wpi::circular_buffer pollStorage; + + // value listeners + VectorSet 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 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, + wpi::span prefixes, + PubSubOptions options) + : handle{handle}, options{std::move(options)} { + this->options.prefixMatch = true; + this->prefixes.reserve(prefixes.size()); + for (auto&& prefix : prefixes) { + this->prefixes.emplace_back(prefix); + } + } + + // invariants + wpi::SignalObject handle; + std::vector prefixes; + PubSubOptions options; + + // value listeners + VectorSet valueListeners; +}; + +struct TopicListenerPollerData { + static constexpr auto kType = Handle::kTopicListenerPoller; + + explicit TopicListenerPollerData(NT_TopicListenerPoller handle) + : handle{handle} {} + + wpi::SignalObject handle; + std::vector queue; +}; + +struct TopicListenerData { + static constexpr auto kType = Handle::kTopicListener; + + TopicListenerData(NT_TopicListener handle, TopicListenerPollerData* poller, + SubscriberData* subscriber, TopicData* topic, + unsigned int eventMask) + : handle{handle}, + poller{poller}, + topic{topic}, + eventMask{eventMask & ~NT_TOPIC_NOTIFY_IMMEDIATE} {} + TopicListenerData(NT_TopicListener handle, TopicListenerPollerData* poller, + MultiSubscriberData* multiSubscriber, + wpi::span prefixes, + unsigned int eventMask) + : handle{handle}, + poller{poller}, + multiSubscriber{multiSubscriber}, + prefixes{prefixes.begin(), prefixes.end()}, + eventMask{eventMask & ~NT_TOPIC_NOTIFY_IMMEDIATE} {} + + wpi::SignalObject handle; + TopicListenerPollerData* poller; + SubscriberData* subscriber{nullptr}; + MultiSubscriberData* multiSubscriber{nullptr}; + TopicData* topic{nullptr}; + std::vector prefixes; + unsigned int eventMask; + bool subscriberOwned{false}; +}; + +struct ValueListenerPollerData { + static constexpr auto kType = Handle::kValueListenerPoller; + + explicit ValueListenerPollerData(NT_ValueListenerPoller handle) + : handle{handle} {} + + wpi::SignalObject handle; + std::vector queue; +}; + +struct ValueListenerData { + static constexpr auto kType = Handle::kValueListener; + + ValueListenerData(NT_ValueListener handle, ValueListenerPollerData* poller, + SubscriberData* subscriber, NT_Handle subentryHandle, + unsigned int eventMask) + : handle{handle}, + poller{poller}, + subscriber{subscriber}, + subentryHandle{subentryHandle}, + eventMask{eventMask & ~NT_VALUE_NOTIFY_IMMEDIATE} {} + + ValueListenerData(NT_ValueListener handle, ValueListenerPollerData* poller, + MultiSubscriberData* subscriber, NT_Handle subentryHandle, + unsigned int eventMask) + : handle{handle}, + poller{poller}, + multiSubscriber{subscriber}, + subentryHandle{subentryHandle}, + eventMask{eventMask & ~NT_VALUE_NOTIFY_IMMEDIATE} {} + + wpi::SignalObject handle; + ValueListenerPollerData* poller; + SubscriberData* subscriber{nullptr}; + MultiSubscriberData* multiSubscriber{nullptr}; + NT_Handle subentryHandle; + unsigned int eventMask; +}; + +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 TopicListenerThread final : public wpi::SafeThreadEvent { + public: + explicit TopicListenerThread(TopicListenerPollerData* pollerData) + : m_pollerData{pollerData}, m_poller{pollerData->handle} {} + + void Main() final; + + TopicListenerPollerData* m_pollerData; + NT_TopicListenerPoller m_poller; + wpi::DenseMap> + m_callbacks; +}; + +struct ValueListenerThread final : public wpi::SafeThreadEvent { + public: + explicit ValueListenerThread(ValueListenerPollerData* pollerData) + : m_pollerData{pollerData}, m_poller{pollerData->handle} {} + + void Main() final; + + ValueListenerPollerData* m_pollerData; + NT_ValueListenerPoller m_poller; + wpi::DenseMap> + m_callbacks; +}; + +struct LSImpl { + LSImpl(int inst, wpi::Logger& logger) : m_inst{inst}, m_logger{logger} {} + + int m_inst; + wpi::Logger& m_logger; + net::NetworkInterface* m_network{nullptr}; + + // handle mappings + HandleMap m_topics; + HandleMap m_publishers; + HandleMap m_subscribers; + HandleMap m_entries; + HandleMap m_multiSubscribers; + HandleMap m_topicListenerPollers; + HandleMap m_topicListeners; + HandleMap m_valueListenerPollers; + HandleMap m_valueListeners; + HandleMap m_dataloggers; + + // name mappings + wpi::StringMap m_nameTopics; + + // string-based listeners + VectorSet m_topicPrefixListeners; + + // callback listener threads + wpi::SafeThreadOwner m_topicListenerThread; + wpi::SafeThreadOwner m_valueListenerThread; + + // topic functions + void NotifyTopic(TopicData* topic, unsigned int eventFlags); + void NotifyTopicListener(TopicListenerData* listener, TopicData* topic, + unsigned int eventFlags); + + void CheckReset(TopicData* topic); + + bool SetValue(TopicData* topic, const Value& value, unsigned int eventFlags); + void NotifyValue(TopicData* topic, unsigned int eventFlags); + void NotifyValueListener(ValueListenerData* listener, TopicData* topic, + unsigned int eventFlags); + + 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); + + 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 RemoveLocalPublisher(NT_Publisher pubHandle); + + SubscriberData* AddLocalSubscriber(TopicData* topic, + const PubSubConfig& options); + std::unique_ptr RemoveLocalSubscriber( + NT_Subscriber subHandle); + + EntryData* AddEntry(SubscriberData* subscriber); + std::unique_ptr RemoveEntry(NT_Entry entryHandle); + + MultiSubscriberData* AddMultiSubscriber( + wpi::span prefixes, const PubSubOptions& options); + std::unique_ptr RemoveMultiSubscriber( + NT_MultiSubscriber subHandle); + + TopicListenerPollerData* AddTopicListenerPoller() { + return m_topicListenerPollers.Add(m_inst); + } + TopicListenerData* AddTopicListenerImpl(TopicListenerPollerData* poller, + TopicData* topic, + unsigned int eventMask); + TopicListenerData* AddTopicListenerImpl(TopicListenerPollerData* poller, + SubscriberData* topic, + unsigned int eventMask); + TopicListenerData* AddTopicListenerImpl(TopicListenerPollerData* poller, + MultiSubscriberData* topic, + unsigned int eventMask); + TopicListenerData* AddTopicListenerImpl( + TopicListenerPollerData* poller, + wpi::span prefixes, unsigned int eventMask); + NT_TopicListener AddTopicListener( + wpi::span prefixes, unsigned int mask, + std::function callback); + NT_TopicListener AddTopicListenerHandle(TopicListenerPollerData* poller, + NT_Handle handle, unsigned int mask); + NT_TopicListener AddTopicListener( + NT_Handle handle, unsigned int mask, + std::function callback); + void DestroyTopicListenerPoller(NT_TopicListenerPoller pollerHandle); + std::unique_ptr RemoveTopicListener( + NT_TopicListener listenerHandle); + + ValueListenerPollerData* AddValueListenerPoller() { + return m_valueListenerPollers.Add(m_inst); + } + ValueListenerData* AddValueListenerImpl(ValueListenerPollerData* poller, + SubscriberData* subscriber, + NT_Handle subentryHandle, + unsigned int eventMask); + ValueListenerData* AddValueListenerImpl(ValueListenerPollerData* poller, + MultiSubscriberData* subscriber, + NT_Handle subentryHandle, + unsigned int eventMask); + NT_ValueListener AddValueListenerHandle(ValueListenerPollerData* poller, + NT_Handle subentryHandle, + unsigned int mask); + NT_ValueListener AddValueListener( + NT_Handle subentry, unsigned int mask, + std::function callback); + void DestroyValueListenerPoller(NT_ValueListenerPoller pollerHandle); + std::unique_ptr RemoveValueListener( + NT_ValueListener listenerHandle); + + 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 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(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) || + ((config.type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE)) != 0 && + (config.type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE)) == + (topic->type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE))) || + ((config.type & (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY)) != + 0 && + (config.type & (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY)) == + (topic->type & + (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY))); +} + +void TopicListenerThread::Main() { + while (m_active) { + WPI_Handle signaledBuf[2]; + auto signaled = + wpi::WaitForObjects({m_poller, m_stopEvent.GetHandle()}, signaledBuf); + if (signaled.empty() || !m_active) { + break; + } + // call all the way back out to the C++ API to ensure valid handle + auto events = nt::ReadTopicListenerQueue(m_poller); + if (events.empty()) { + continue; + } + 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(); + } + } + } +} + +void ValueListenerThread::Main() { + while (m_active) { + WPI_Handle signaledBuf[2]; + auto signaled = + wpi::WaitForObjects({m_poller, m_stopEvent.GetHandle()}, signaledBuf); + if (signaled.empty() || !m_active) { + break; + } + // call all the way back out to the C++ API to ensure valid handle + auto events = nt::ReadValueListenerQueue(m_poller); + if (events.empty()) { + continue; + } + 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(); + } + } + } +} + +void LSImpl::NotifyTopic(TopicData* topic, unsigned int eventFlags) { + for (auto&& listener : topic->listeners) { + NotifyTopicListener(listener, topic, eventFlags); + } + + for (auto listener : m_topicPrefixListeners) { + for (auto&& prefix : listener->prefixes) { + if (wpi::starts_with(topic->name, prefix)) { + NotifyTopicListener(listener, topic, eventFlags); + } + } + } + + if ((eventFlags & (NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_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_TOPIC_NOTIFY_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_TOPIC_NOTIFY_UNPUBLISH) != 0 && + it != topic->datalogs.end()) { + it->log->Finish(it->entry, now); + topic->datalogType = NT_UNASSIGNED; + topic->datalogs.erase(it); + } + } + } + } + } else if ((eventFlags & NT_TOPIC_NOTIFY_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::NotifyTopicListener(TopicListenerData* listener, TopicData* topic, + unsigned int eventFlags) { + // filter by event mask + if ((eventFlags & listener->eventMask) == 0) { + return; + } + + listener->poller->queue.emplace_back(listener->handle, topic->GetTopicInfo(), + eventFlags); + listener->handle.Set(); + listener->poller->handle.Set(); +} + +void LSImpl::CheckReset(TopicData* topic) { + if (topic->Exists()) { + return; + } + topic->lastValue = {}; + 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) { + 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); + } + if (topic->datalogType == value.type()) { + for (auto&& datalog : topic->datalogs) { + datalog.Append(value); + } + } + return true; +} + +void LSImpl::NotifyValue(TopicData* topic, unsigned int eventFlags) { + for (auto&& subscriber : topic->localSubscribers) { + if (subscriber->active) { + subscriber->pollStorage.emplace_back(topic->lastValue); + subscriber->handle.Set(); + for (auto&& listener : subscriber->valueListeners) { + NotifyValueListener(listener, topic, eventFlags); + } + } + } + + for (auto&& subscriber : topic->multiSubscribers) { + subscriber->handle.Set(); + for (auto&& listener : subscriber->valueListeners) { + NotifyValueListener(listener, topic, eventFlags); + } + } +} + +void LSImpl::NotifyValueListener(ValueListenerData* listener, TopicData* topic, + unsigned int eventFlags) { + // only notify listener for local events if it wants local events + bool listenLocal = (listener->eventMask & NT_VALUE_NOTIFY_LOCAL) != 0; + bool eventLocal = (eventFlags & NT_VALUE_NOTIFY_LOCAL) != 0; + if (eventLocal && !listenLocal) { + return; + } + + listener->poller->queue.emplace_back(listener->handle, topic->handle, + listener->subentryHandle, + topic->lastValue, eventFlags); + listener->handle.Set(); + listener->poller->handle.Set(); +} + +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_TOPIC_NOTIFY_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_TOPIC_NOTIFY_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_TOPIC_NOTIFY_NONE, true, false); +} + +void LSImpl::SetProperties(TopicData* topic, const wpi::json& update, + bool sendNetwork) { + 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_TOPIC_NOTIFY_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()) { + 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()) { + if (*val) { + topic->flags |= NT_RETAINED; + } else { + topic->flags &= ~NT_RETAINED; + } + } + } + } + + topic->propertiesStr = topic->properties.dump(); + NotifyTopic(topic, eventFlags | NT_TOPIC_NOTIFY_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) { + for (auto&& publisher : topic->localPublishers) { + publisher->UpdateActive(); + } + for (auto&& subscriber : topic->localSubscribers) { + subscriber->UpdateActive(); + } +} + +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 notify = NT_TOPIC_NOTIFY_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); + } + if (!didExist) { + notify |= NT_TOPIC_NOTIFY_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, notify, false); + } else if (notify != NT_TOPIC_NOTIFY_NONE) { + NotifyTopic(topic, notify); + } +} + +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_TOPIC_NOTIFY_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); + // 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) { + // set the type to the published type + topic->type = config.type; + topic->typeStr = config.typeStr; + RefreshPubSubActive(topic); + + if (properties.is_null()) { + topic->properties = wpi::json::object(); + } else { + topic->properties = properties; + } + + if (topic->properties.empty()) { + NotifyTopic(topic, NT_TOPIC_NOTIFY_PUBLISH); + } else { + PropertiesUpdated(topic, topic->properties, NT_TOPIC_NOTIFY_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 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_TOPIC_NOTIFY_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); + 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) { + 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 '{}', " + "currently '{}')", + topic->name, config.typeStr, topic->typeStr); + } + if (m_network) { + m_network->Subscribe(subscriber->handle, {{topic->name}}, config); + } + return subscriber; +} + +std::unique_ptr 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 : subscriber->valueListeners) { + listener->subscriber = nullptr; + } + // shouldn't be necessary, but do it anyway + for (auto&& listener : m_topicListeners) { + if (listener->subscriber == subscriber.get()) { + listener->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 LSImpl::RemoveEntry(NT_Entry entryHandle) { + auto entry = m_entries.Remove(entryHandle); + if (entry) { + entry->topic->entries.Remove(entry.get()); + } + return entry; +} + +MultiSubscriberData* LSImpl::AddMultiSubscriber( + wpi::span 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 (wpi::starts_with(topic->name, prefix)) { + topic->multiSubscribers.Add(subscriber); + break; + } + } + } + if (m_network) { + m_network->Subscribe(subscriber->handle, subscriber->prefixes, + subscriber->options); + } + return subscriber; +} + +std::unique_ptr 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 : subscriber->valueListeners) { + listener->multiSubscriber = nullptr; + } + // shouldn't be necessary, but do it anyway + for (auto&& listener : m_topicListeners) { + if (listener->multiSubscriber == subscriber.get()) { + listener->multiSubscriber = nullptr; + } + } + if (m_network) { + m_network->Unsubscribe(subscriber->handle); + } + } + return subscriber; +} + +TopicListenerData* LSImpl::AddTopicListenerImpl(TopicListenerPollerData* poller, + TopicData* topic, + unsigned int eventMask) { + // subscribe to make sure topic updates are received + PubSubConfig config; + config.topicsOnly = true; + auto subscriber = AddLocalSubscriber(topic, config); + auto listener = AddTopicListenerImpl(poller, subscriber, eventMask); + listener->subscriberOwned = true; + return listener; +} + +TopicListenerData* LSImpl::AddTopicListenerImpl(TopicListenerPollerData* poller, + SubscriberData* subscriber, + unsigned int eventMask) { + auto listener = m_topicListeners.Add(m_inst, poller, subscriber, + subscriber->topic, eventMask); + subscriber->topic->listeners.Add(listener); + + // handle immediate + if ((eventMask & (NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE)) == + (NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE) && + listener->topic->Exists()) { + listener->poller->queue.emplace_back( + listener->handle, subscriber->topic->GetTopicInfo(), + NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE); + listener->handle.Set(); + listener->poller->handle.Set(); + } + + return listener; +} + +TopicListenerData* LSImpl::AddTopicListenerImpl(TopicListenerPollerData* poller, + MultiSubscriberData* subscriber, + unsigned int eventMask) { + auto listener = m_topicListeners.Add(m_inst, poller, subscriber, + subscriber->prefixes, eventMask); + m_topicPrefixListeners.Add(listener); + + // handle immediate + if ((eventMask & (NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE)) == + (NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE)) { + bool any = false; + for (auto&& topic : m_topics) { + for (auto&& prefix : listener->multiSubscriber->prefixes) { + if (wpi::starts_with(topic->name, prefix) && topic->Exists()) { + listener->poller->queue.emplace_back( + listener->handle, topic->GetTopicInfo(), + NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE); + any = true; + } + } + } + if (any) { + listener->handle.Set(); + listener->poller->handle.Set(); + } + } + + return listener; +} + +TopicListenerData* LSImpl::AddTopicListenerImpl( + TopicListenerPollerData* poller, wpi::span prefixes, + unsigned int eventMask) { + // subscribe to make sure topic updates are received + PubSubOptions options; + options.topicsOnly = true; + options.prefixMatch = true; + auto subscriber = AddMultiSubscriber(prefixes, options); + auto listener = AddTopicListenerImpl(poller, subscriber, eventMask); + listener->subscriberOwned = true; + return listener; +} + +NT_TopicListener LSImpl::AddTopicListener( + wpi::span prefixes, unsigned int mask, + std::function callback) { + if (!m_topicListenerThread) { + m_topicListenerThread.Start(AddTopicListenerPoller()); + } + if (auto thr = m_topicListenerThread.GetThread()) { + NT_TopicListener listener = + AddTopicListenerImpl(thr->m_pollerData, prefixes, mask)->handle; + if (listener) { + thr->m_callbacks.try_emplace(listener, std::move(callback)); + } + return listener; + } else { + return {}; + } +} + +NT_TopicListener LSImpl::AddTopicListenerHandle(TopicListenerPollerData* poller, + NT_Handle handle, + unsigned int mask) { + if (auto topic = m_topics.Get(handle)) { + return AddTopicListenerImpl(poller, topic, mask)->handle; + } else if (auto sub = m_multiSubscribers.Get(handle)) { + return AddTopicListenerImpl(poller, sub, mask)->handle; + } else if (auto sub = m_subscribers.Get(handle)) { + return AddTopicListenerImpl(poller, sub, mask)->handle; + } else if (auto entry = m_entries.Get(handle)) { + return AddTopicListenerImpl(poller, entry->subscriber, mask)->handle; + } else { + return {}; + } +} + +NT_TopicListener LSImpl::AddTopicListener( + NT_Handle handle, unsigned int mask, + std::function callback) { + if (!m_topicListenerThread) { + m_topicListenerThread.Start(AddTopicListenerPoller()); + } + if (auto thr = m_topicListenerThread.GetThread()) { + NT_TopicListener listener = + AddTopicListenerHandle(thr->m_pollerData, handle, mask); + if (listener) { + thr->m_callbacks.try_emplace(listener, std::move(callback)); + } + return listener; + } else { + return {}; + } +} + +void LSImpl::DestroyTopicListenerPoller(NT_TopicListenerPoller pollerHandle) { + if (auto poller = m_topicListenerPollers.Remove(pollerHandle)) { + // ensure all listeners that use this poller are removed + wpi::SmallVector toRemove; + for (auto&& listener : m_topicListeners) { + if (listener->poller == poller.get()) { + toRemove.emplace_back(listener->handle); + } + } + for (auto handle : toRemove) { + RemoveTopicListener(handle); + } + } +} + +std::unique_ptr LSImpl::RemoveTopicListener( + NT_TopicListener listenerHandle) { + auto listener = m_topicListeners.Remove(listenerHandle); + if (listener) { + if (listener->subscriberOwned) { + if (listener->subscriber) { + RemoveLocalSubscriber(listener->subscriber->handle); + } + if (listener->multiSubscriber) { + RemoveMultiSubscriber(listener->multiSubscriber->handle); + } + } + if (listener->topic) { + listener->topic->listeners.Remove(listener.get()); + } else { + m_topicPrefixListeners.Remove(listener.get()); + } + } + return listener; +} + +ValueListenerData* LSImpl::AddValueListenerImpl(ValueListenerPollerData* poller, + SubscriberData* subscriber, + NT_Handle subentryHandle, + unsigned int eventMask) { + auto listener = m_valueListeners.Add(m_inst, poller, subscriber, + subentryHandle, eventMask); + listener->subscriber->valueListeners.Add(listener); + auto topic = subscriber->topic; + + // handle immediate + if ((eventMask & NT_VALUE_NOTIFY_IMMEDIATE) != 0 && topic->lastValue) { + listener->poller->queue.emplace_back(listener->handle, topic->handle, + subentryHandle, topic->lastValue, + NT_VALUE_NOTIFY_IMMEDIATE); + listener->handle.Set(); + listener->poller->handle.Set(); + } + + return listener; +} + +ValueListenerData* LSImpl::AddValueListenerImpl(ValueListenerPollerData* poller, + MultiSubscriberData* subscriber, + NT_Handle subentryHandle, + unsigned int eventMask) { + auto listener = m_valueListeners.Add(m_inst, poller, subscriber, + subentryHandle, eventMask); + listener->multiSubscriber->valueListeners.Add(listener); + + // handle immediate + if ((eventMask & NT_VALUE_NOTIFY_IMMEDIATE) != 0) { + bool any = false; + for (auto&& topic : m_topics) { + for (auto&& prefix : listener->multiSubscriber->prefixes) { + if (wpi::starts_with(topic->name, prefix) && topic->lastValue) { + listener->poller->queue.emplace_back(listener->handle, topic->handle, + subentryHandle, topic->lastValue, + NT_VALUE_NOTIFY_IMMEDIATE); + any = true; + } + } + } + if (any) { + listener->handle.Set(); + listener->poller->handle.Set(); + } + } + + return listener; +} + +NT_ValueListener LSImpl::AddValueListenerHandle(ValueListenerPollerData* poller, + NT_Handle subentryHandle, + unsigned int mask) { + if (auto sub = m_subscribers.Get(subentryHandle)) { + return AddValueListenerImpl(poller, sub, subentryHandle, mask)->handle; + } else if (auto entry = m_entries.Get(subentryHandle)) { + return AddValueListenerImpl(poller, entry->subscriber, subentryHandle, mask) + ->handle; + } else if (auto sub = m_multiSubscribers.Get(subentryHandle)) { + return AddValueListenerImpl(poller, sub, subentryHandle, mask)->handle; + } else { + return {}; + } +} + +NT_ValueListener LSImpl::AddValueListener( + NT_Handle subentry, unsigned int mask, + std::function callback) { + if (!m_valueListenerThread) { + m_valueListenerThread.Start(AddValueListenerPoller()); + } + if (auto thr = m_valueListenerThread.GetThread()) { + auto listener = AddValueListenerHandle(thr->m_pollerData, subentry, mask); + if (listener) { + thr->m_callbacks.try_emplace(listener, std::move(callback)); + } + return listener; + } else { + return {}; + } +} + +void LSImpl::DestroyValueListenerPoller(NT_ValueListenerPoller pollerHandle) { + if (auto poller = m_valueListenerPollers.Remove(pollerHandle)) { + // ensure all listeners that use this poller are removed + wpi::SmallVector toRemove; + for (auto&& listener : m_valueListeners) { + if (listener->poller == poller.get()) { + toRemove.emplace_back(listener->handle); + } + } + for (auto handle : toRemove) { + RemoveValueListener(handle); + } + } +} + +std::unique_ptr LSImpl::RemoveValueListener( + NT_ValueListener listenerHandle) { + auto listener = m_valueListeners.Remove(listenerHandle); + if (listener) { + if (listener->subscriber) { + listener->subscriber->valueListeners.Remove(listener.get()); + } + if (listener->multiSubscriber) { + listener->multiSubscriber->valueListeners.Remove(listener.get()); + } + } + return listener; +} + +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) { + for (auto&& prefix : sub->prefixes) { + if (wpi::starts_with(name, prefix)) { + topic->multiSubscribers.Add(sub.get()); + break; + } + } + } + } + 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) { + // don't allow dynamically changing the type of an entry + 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) { + if (!value) { + return false; + } + if (publisher->topic->type != NT_UNASSIGNED && + publisher->topic->type != value.type()) { + return false; + } + if (publisher->active) { + if (m_network) { + m_network->SetValue(publisher->handle, value); + } + return SetValue(publisher->topic, value, NT_VALUE_NOTIFY_LOCAL); + } 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 (!publisher) { + return false; + } + } + return PublishLocalValue(publisher, value); +} + +bool LSImpl::SetDefaultEntryValue(NT_Handle pubsubentryHandle, + const Value& value) { + if (!value) { + return false; + } + if (auto topic = GetTopic(pubsubentryHandle)) { + if (topic->type == NT_UNASSIGNED || topic->type == value.type()) { + // set without notifying + topic->type = value.type(); + topic->lastValue = value; + topic->lastValue.SetTime(0); + topic->lastValue.SetServerTime(0); + + auto publisher = m_publishers.Get(pubsubentryHandle); + if (!publisher) { + if (auto entry = m_entries.Get(pubsubentryHandle)) { + publisher = PublishEntry(entry, value.type()); + } + if (!publisher) { + return true; + } + } + if (publisher->active && m_network) { + m_network->SetValue(publisher->handle, value); + } + 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, wpi::Logger& logger) : LSImpl{inst, logger} {} +}; + +LocalStorage::LocalStorage(int inst, wpi::Logger& logger) + : m_impl{std::make_unique(inst, 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)) { + m_impl->SetValue(topic, value, NT_VALUE_NOTIFY_NONE); + } +} + +void LocalStorage::StartNetwork(net::NetworkStartupInterface& startup) { + std::scoped_lock lock{m_mutex}; + // 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) { + startup.Publish(publisher->handle, topic->handle, topic->name, + topic->typeStr, topic->properties, publisher->config); + anyPublisher = publisher; + } + } + if (anyPublisher && topic->lastValue) { + startup.SetValue(anyPublisher->handle, topic->lastValue); + } + } + for (auto&& subscriber : m_impl->m_subscribers) { + if (subscriber->active) { + startup.Subscribe(subscriber->handle, {{subscriber->topic->name}}, + subscriber->config); + } + } + for (auto&& subscriber : m_impl->m_multiSubscribers) { + startup.Subscribe(subscriber->handle, subscriber->prefixes, + subscriber->options); + } +} + +void LocalStorage::SetNetwork(net::NetworkInterface* network) { + std::scoped_lock lock{m_mutex}; + m_impl->m_network = network; +} + +void LocalStorage::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 +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 +static void ForEachTopic(T& topics, std::string_view prefix, + wpi::span 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 LocalStorage::GetTopics(std::string_view prefix, + unsigned int types) { + std::scoped_lock lock(m_mutex); + std::vector rv; + ForEachTopic(m_impl->m_topics, prefix, types, + [&](TopicData& topic) { rv.push_back(topic.handle); }); + return rv; +} + +std::vector LocalStorage::GetTopics( + std::string_view prefix, wpi::span types) { + std::scoped_lock lock(m_mutex); + std::vector rv; + ForEachTopic(m_impl->m_topics, prefix, types, + [&](TopicData& topic) { rv.push_back(topic.handle); }); + return rv; +} + +std::vector LocalStorage::GetTopicInfo(std::string_view prefix, + unsigned int types) { + std::scoped_lock lock(m_mutex); + std::vector rv; + ForEachTopic(m_impl->m_topics, prefix, types, [&](TopicData& topic) { + rv.emplace_back(topic.GetTopicInfo()); + }); + return rv; +} + +std::vector LocalStorage::GetTopicInfo( + std::string_view prefix, wpi::span types) { + std::scoped_lock lock(m_mutex); + std::vector 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_TOPIC_NOTIFY_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_TOPIC_NOTIFY_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(); + } +} + +void LocalStorage::SetTopicProperties(NT_Topic topicHandle, + const wpi::json& update) { + if (!update.is_object()) { + return; + } + std::scoped_lock lock{m_mutex}; + if (auto topic = m_impl->m_topics.Get(topicHandle)) { + m_impl->SetProperties(topic, update, true); + } +} + +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, + wpi::span options) { + std::scoped_lock lock{m_mutex}; + + // Get the topic + auto* topic = m_impl->m_topics.Get(topicHandle); + if (!topic) { + 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( + wpi::span prefixes, + wpi::span options) { + std::scoped_lock lock{m_mutex}; + PubSubOptions opts{options}; + opts.prefixMatch = true; + return m_impl->AddMultiSubscriber(prefixes, opts)->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, + wpi::span options) { + if (type == NT_UNASSIGNED || typeStr.empty()) { + return 0; + } + + std::scoped_lock lock{m_mutex}; + + // Get the topic + auto* topic = m_impl->m_topics.Get(topicHandle); + if (!topic) { + 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); + } + } else { + // TODO: report warning + return; + } +} + +NT_Entry LocalStorage::GetEntry(NT_Topic topicHandle, NT_Type type, + std::string_view typeStr, + wpi::span options) { + std::scoped_lock lock{m_mutex}; + + // Get the topic + auto* topic = m_impl->m_topics.Get(topicHandle); + if (!topic) { + 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& 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 +static T GetAtomicNumber(Value* value, U defaultValue) { + if (value && value->type() == NT_INTEGER) { + return {value->time(), value->server_time(), + static_cast(value->GetInteger())}; + } else if (value && value->type() == NT_FLOAT) { + return {value->time(), value->server_time(), + static_cast(value->GetFloat())}; + } else if (value && value->type() == NT_DOUBLE) { + return {value->time(), value->server_time(), + static_cast(value->GetDouble())}; + } else { + return {0, 0, defaultValue}; + } +} + +template +static T GetAtomicNumberArray(Value* value, wpi::span 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 +static T GetAtomicNumberArray(Value* value, wpi::SmallVectorImpl& buf, + wpi::span 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( \ + m_impl->GetSubEntryValue(subentry), defaultValue); \ + } \ + \ + Timestamped##Name##Array LocalStorage::GetAtomic##Name##Array( \ + NT_Handle subentry, wpi::span defaultValue) { \ + std::scoped_lock lock{m_mutex}; \ + return GetAtomicNumberArray( \ + m_impl->GetSubEntryValue(subentry), defaultValue); \ + } \ + \ + Timestamped##Name##ArrayView LocalStorage::GetAtomic##Name##Array( \ + NT_Handle subentry, wpi::SmallVectorImpl& buf, \ + wpi::span defaultValue) { \ + std::scoped_lock lock{m_mutex}; \ + return GetAtomicNumberArray( \ + 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, wpi::span 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& buf, \ + wpi::span 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 LocalStorage::ReadQueueValue(NT_Handle subentry) { + std::scoped_lock lock{m_mutex}; + auto subscriber = m_impl->GetSubEntry(subentry); + if (!subscriber) { + return {}; + } + std::vector rv; + rv.reserve(subscriber->pollStorage.size()); + for (auto&& val : subscriber->pollStorage) { + rv.emplace_back(std::move(val)); + } + subscriber->pollStorage.reset(); + return rv; +} + +std::vector LocalStorage::ReadQueueBoolean( + NT_Handle subentry) { + std::scoped_lock lock{m_mutex}; + auto subscriber = m_impl->GetSubEntry(subentry); + if (!subscriber) { + return {}; + } + std::vector 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 LocalStorage::ReadQueueString( + NT_Handle subentry) { + std::scoped_lock lock{m_mutex}; + auto subscriber = m_impl->GetSubEntry(subentry); + if (!subscriber) { + return {}; + } + std::vector 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 LocalStorage::ReadQueue##Name( \ + NT_Handle subentry) { \ + std::scoped_lock lock{m_mutex}; \ + auto subscriber = m_impl->GetSubEntry(subentry); \ + if (!subscriber) { \ + return {}; \ + } \ + std::vector 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 +static std::vector ReadQueueNumber(SubscriberData* subscriber) { + if (!subscriber) { + return {}; + } + std::vector 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 +static std::vector ReadQueueNumberArray(SubscriberData* subscriber) { + if (!subscriber) { + return {}; + } + std::vector 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 LocalStorage::ReadQueue##Name( \ + NT_Handle subentry) { \ + std::scoped_lock lock{m_mutex}; \ + return ReadQueueNumber(m_impl->GetSubEntry(subentry)); \ + } \ + \ + std::vector LocalStorage::ReadQueue##Name##Array( \ + NT_Handle subentry) { \ + std::scoped_lock lock{m_mutex}; \ + return ReadQueueNumberArray( \ + 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)) { + return subscriber->topic->lastValue; + } else { + 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); + + // Create subscriber + auto* subscriber = m_impl->AddLocalSubscriber(topic, {}); + + // Create entry + return m_impl->AddEntry(subscriber)->handle; +} + +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; + } +} + +NT_TopicListener LocalStorage::AddTopicListener( + wpi::span prefixes, unsigned int mask, + std::function callback) { + std::scoped_lock lock{m_mutex}; + return m_impl->AddTopicListener(prefixes, mask, std::move(callback)); +} + +NT_TopicListener LocalStorage::AddTopicListener( + NT_Topic topicHandle, unsigned int mask, + std::function callback) { + std::scoped_lock lock{m_mutex}; + return m_impl->AddTopicListener(topicHandle, mask, std::move(callback)); +} + +NT_TopicListenerPoller LocalStorage::CreateTopicListenerPoller() { + std::scoped_lock lock{m_mutex}; + return m_impl->AddTopicListenerPoller()->handle; +} + +void LocalStorage::DestroyTopicListenerPoller( + NT_TopicListenerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + m_impl->DestroyTopicListenerPoller(pollerHandle); +} + +NT_TopicListener LocalStorage::AddPolledTopicListener( + NT_TopicListenerPoller pollerHandle, + wpi::span prefixes, unsigned int mask) { + std::scoped_lock lock{m_mutex}; + if (auto poller = m_impl->m_topicListenerPollers.Get(pollerHandle)) { + return m_impl->AddTopicListenerImpl(poller, prefixes, mask)->handle; + } else { + return {}; + } +} + +NT_TopicListener LocalStorage::AddPolledTopicListener( + NT_TopicListenerPoller pollerHandle, NT_Handle handle, unsigned int mask) { + std::scoped_lock lock{m_mutex}; + + auto poller = m_impl->m_topicListenerPollers.Get(pollerHandle); + if (!poller) { + return {}; + } + + return m_impl->AddTopicListenerHandle(poller, handle, mask); +} + +std::vector LocalStorage::ReadTopicListenerQueue( + NT_TopicListenerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + if (auto poller = m_impl->m_topicListenerPollers.Get(pollerHandle)) { + std::vector rv; + rv.swap(poller->queue); + return rv; + } else { + return {}; + } +} + +void LocalStorage::RemoveTopicListener(NT_TopicListener listenerHandle) { + std::scoped_lock lock{m_mutex}; + m_impl->RemoveTopicListener(listenerHandle); +} + +NT_ValueListener LocalStorage::AddValueListener( + NT_Handle subentry, unsigned int mask, + std::function callback) { + std::scoped_lock lock{m_mutex}; + return m_impl->AddValueListener(subentry, mask, std::move(callback)); +} + +NT_ValueListenerPoller LocalStorage::CreateValueListenerPoller() { + std::scoped_lock lock{m_mutex}; + return m_impl->AddValueListenerPoller()->handle; +} + +void LocalStorage::DestroyValueListenerPoller( + NT_ValueListenerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + m_impl->DestroyValueListenerPoller(pollerHandle); +} + +NT_ValueListener LocalStorage::AddPolledValueListener( + NT_ValueListenerPoller pollerHandle, NT_Handle subentryHandle, + unsigned int mask) { + std::scoped_lock lock{m_mutex}; + + auto poller = m_impl->m_valueListenerPollers.Get(pollerHandle); + if (!poller) { + return {}; + } + return m_impl->AddValueListenerHandle(poller, subentryHandle, mask); +} + +std::vector LocalStorage::ReadValueListenerQueue( + NT_ValueListenerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + if (auto poller = m_impl->m_valueListenerPollers.Get(pollerHandle)) { + std::vector rv; + rv.swap(poller->queue); + return rv; + } else { + return {}; + } +} + +void LocalStorage::RemoveValueListener(NT_ValueListener listenerHandle) { + std::scoped_lock lock{m_mutex}; + m_impl->RemoveValueListener(listenerHandle); +} + +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); + } + } + } +} diff --git a/ntcore/src/main/native/cpp/LocalStorage.h b/ntcore/src/main/native/cpp/LocalStorage.h new file mode 100644 index 0000000000..503783c69d --- /dev/null +++ b/ntcore/src/main/native/cpp/LocalStorage.h @@ -0,0 +1,250 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "net/NetworkInterface.h" +#include "ntcore_cpp.h" + +namespace wpi { +class Logger; +} // namespace wpi + +namespace nt { + +class LocalStorage final : public net::ILocalStorage { + public: + LocalStorage(int inst, 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::NetworkStartupInterface& startup) final; + void SetNetwork(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 GetTopics(std::string_view prefix, unsigned int types); + std::vector GetTopics(std::string_view prefix, + wpi::span types); + + std::vector GetTopicInfo(std::string_view prefix, + unsigned int types); + std::vector GetTopicInfo(std::string_view prefix, + wpi::span 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); + + void 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, + wpi::span options); + + void Unsubscribe(NT_Subscriber sub); + + NT_MultiSubscriber SubscribeMultiple( + wpi::span prefixes, + wpi::span options); + + void UnsubscribeMultiple(NT_MultiSubscriber subHandle); + + NT_Publisher Publish(NT_Topic topic, NT_Type type, std::string_view typeStr, + const wpi::json& properties, + wpi::span options); + + void Unpublish(NT_Handle pubentry); + + NT_Entry GetEntry(NT_Topic topic, NT_Type type, std::string_view typeStr, + wpi::span 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, + wpi::span defaultValue); + TimestampedBooleanArray GetAtomicBooleanArray( + NT_Handle subentry, wpi::span defaultValue); + TimestampedIntegerArray GetAtomicIntegerArray( + NT_Handle subentry, wpi::span defaultValue); + TimestampedFloatArray GetAtomicFloatArray( + NT_Handle subentry, wpi::span defaultValue); + TimestampedDoubleArray GetAtomicDoubleArray( + NT_Handle subentry, wpi::span defaultValue); + TimestampedStringArray GetAtomicStringArray( + NT_Handle subentry, wpi::span defaultValue); + + TimestampedStringView GetAtomicString(NT_Handle subentry, + wpi::SmallVectorImpl& buf, + std::string_view defaultValue); + TimestampedRawView GetAtomicRaw(NT_Handle subentry, + wpi::SmallVectorImpl& buf, + wpi::span defaultValue); + TimestampedBooleanArrayView GetAtomicBooleanArray( + NT_Handle subentry, wpi::SmallVectorImpl& buf, + wpi::span defaultValue); + TimestampedIntegerArrayView GetAtomicIntegerArray( + NT_Handle subentry, wpi::SmallVectorImpl& buf, + wpi::span defaultValue); + TimestampedFloatArrayView GetAtomicFloatArray( + NT_Handle subentry, wpi::SmallVectorImpl& buf, + wpi::span defaultValue); + TimestampedDoubleArrayView GetAtomicDoubleArray( + NT_Handle subentry, wpi::SmallVectorImpl& buf, + wpi::span defaultValue); + + std::vector ReadQueueValue(NT_Handle subentry); + + std::vector ReadQueueBoolean(NT_Handle subentry); + std::vector ReadQueueInteger(NT_Handle subentry); + std::vector ReadQueueFloat(NT_Handle subentry); + std::vector ReadQueueDouble(NT_Handle subentry); + std::vector ReadQueueString(NT_Handle subentry); + std::vector ReadQueueRaw(NT_Handle subentry); + std::vector ReadQueueBooleanArray( + NT_Handle subentry); + std::vector ReadQueueIntegerArray( + NT_Handle subentry); + std::vector ReadQueueFloatArray(NT_Handle subentry); + std::vector ReadQueueDoubleArray(NT_Handle subentry); + std::vector 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); + + // + // Topic listener functions + // + + NT_TopicListener AddTopicListener( + wpi::span prefixes, unsigned int mask, + std::function callback); + NT_TopicListener AddTopicListener( + NT_Handle handle, unsigned int mask, + std::function callback); + + NT_TopicListenerPoller CreateTopicListenerPoller(); + void DestroyTopicListenerPoller(NT_TopicListenerPoller poller); + + NT_TopicListener AddPolledTopicListener( + NT_TopicListenerPoller poller, wpi::span prefixes, + unsigned int mask); + NT_TopicListener AddPolledTopicListener(NT_TopicListenerPoller poller, + NT_Handle handle, unsigned int mask); + + std::vector ReadTopicListenerQueue( + NT_TopicListenerPoller poller); + + void RemoveTopicListener(NT_TopicListener listener); + + // + // Value listener functions + // + + NT_ValueListener AddValueListener( + NT_Handle subentry, unsigned int mask, + std::function callback); + + NT_ValueListenerPoller CreateValueListenerPoller(); + void DestroyValueListenerPoller(NT_ValueListenerPoller poller); + + NT_ValueListener AddPolledValueListener(NT_ValueListenerPoller poller, + NT_Handle subentry, + unsigned int mask); + + std::vector ReadValueListenerQueue( + NT_ValueListenerPoller poller); + + void RemoveValueListener(NT_ValueListener listener); + + // + // Data log functions + // + NT_DataLogger StartDataLog(wpi::log::DataLog& log, std::string_view prefix, + std::string_view logPrefix); + void StopDataLog(NT_DataLogger logger); + + private: + class Impl; + std::unique_ptr 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 d3066d7414..daa4d670e8 100644 --- a/ntcore/src/main/native/cpp/Log.h +++ b/ntcore/src/main/native/cpp/Log.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_LOG_H_ -#define NTCORE_LOG_H_ +#pragma once #include @@ -19,5 +18,3 @@ #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_ diff --git a/ntcore/src/main/native/cpp/LoggerImpl.cpp b/ntcore/src/main/native/cpp/LoggerImpl.cpp index fcb59d613e..2e3e24f3ba 100644 --- a/ntcore/src/main/native/cpp/LoggerImpl.cpp +++ b/ntcore/src/main/native/cpp/LoggerImpl.cpp @@ -5,6 +5,7 @@ #include "LoggerImpl.h" #include +#include #include using namespace nt; @@ -29,34 +30,108 @@ static void DefaultLogger(unsigned int level, const char* file, fmt::print(stderr, "NT: {}: {} ({}:{})\n", levelmsg, msg, file, line); } -LoggerImpl::LoggerImpl(int inst) : m_inst(inst) {} +class LoggerImpl::Thread final : public wpi::SafeThreadEvent { + public: + explicit Thread(NT_LoggerPoller poller) : m_poller{poller} {} -void LoggerImpl::Start() { - DoStart(m_inst); + void Main() final; + + NT_LoggerPoller m_poller; + wpi::DenseMap> + m_callbacks; +}; + +void LoggerImpl::Thread::Main() { + while (m_active) { + WPI_Handle signaledBuf[2]; + auto signaled = + wpi::WaitForObjects({m_poller, m_stopEvent.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::ReadLoggerQueue(m_poller); + if (events.empty()) { + continue; + } + std::unique_lock lock{m_mutex}; + for (auto&& event : events) { + auto callbackIt = m_callbacks.find(event.logger); + if (callbackIt != m_callbacks.end()) { + auto callback = callbackIt->second; + lock.unlock(); + callback(event); + lock.lock(); + } + } + } } -unsigned int LoggerImpl::Add( - std::function callback, unsigned int min_level, - unsigned int max_level) { - return DoAdd(callback, min_level, max_level); +LoggerImpl::LoggerImpl(int inst) : m_inst{inst} {} + +LoggerImpl::~LoggerImpl() = default; + +NT_Logger LoggerImpl::Add(std::function callback, + unsigned int minLevel, unsigned int maxLevel) { + if (!m_thread) { + m_thread.Start(CreatePoller()); + } + if (auto thr = m_thread.GetThread()) { + auto listener = AddPolled(thr->m_poller, minLevel, maxLevel); + if (listener) { + thr->m_callbacks.try_emplace(listener, std::move(callback)); + } + return listener; + } else { + return {}; + } } -unsigned int LoggerImpl::AddPolled(unsigned int poller_uid, - unsigned int min_level, - unsigned int max_level) { - return DoAdd(poller_uid, min_level, max_level); +NT_LoggerPoller LoggerImpl::CreatePoller() { + std::scoped_lock lock{m_mutex}; + return m_pollers.Add(m_inst)->handle; +} + +void LoggerImpl::DestroyPoller(NT_LoggerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + m_pollers.Remove(pollerHandle); +} + +NT_Logger LoggerImpl::AddPolled(NT_LoggerPoller pollerHandle, + unsigned int minLevel, unsigned int maxLevel) { + std::scoped_lock lock{m_mutex}; + if (auto poller = m_pollers.Get(pollerHandle)) { + return m_listeners.Add(m_inst, poller, minLevel, maxLevel)->handle; + } else { + return {}; + } +} + +std::vector LoggerImpl::ReadQueue(NT_LoggerPoller pollerHandle) { + std::scoped_lock lock{m_mutex}; + if (auto poller = m_pollers.Get(pollerHandle)) { + std::vector rv; + rv.swap(poller->queue); + return rv; + } else { + return {}; + } +} + +void LoggerImpl::Remove(NT_Logger listenerHandle) { + std::scoped_lock lock{m_mutex}; + m_listeners.Remove(listenerHandle); + if (auto thr = m_thread.GetThread()) { + thr->m_callbacks.erase(listenerHandle); + } } unsigned int LoggerImpl::GetMinLevel() { - auto thr = GetThread(); - if (!thr) { - return NT_LOG_INFO; - } + // return 0; 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&& listener : m_listeners) { + if (listener && listener->minLevel < level) { + level = listener->minLevel; } } return level; @@ -66,10 +141,18 @@ 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()) { + std::scoped_lock lock{m_mutex}; + if (m_listeners.empty()) { DefaultLogger(level, filename.string().c_str(), line, msg); + } else { + for (auto&& listener : m_listeners) { + if (level >= listener->minLevel && level <= listener->maxLevel) { + listener->poller->queue.emplace_back(listener->handle.GetHandle(), + level, file, line, msg); + listener->poller->handle.Set(); + listener->handle.Set(); + } + } } } - 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 2b577c1199..141e0c8629 100644 --- a/ntcore/src/main/native/cpp/LoggerImpl.h +++ b/ntcore/src/main/native/cpp/LoggerImpl.h @@ -2,76 +2,36 @@ // Open Source 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 +#include -#include +#include +#include +#include #include "Handle.h" +#include "HandleMap.h" +#include "ntcore_c.h" #include "ntcore_cpp.h" namespace nt { -namespace impl { - -struct LoggerListenerData : public wpi::CallbackListenerData< - std::function> { - LoggerListenerData() = default; - LoggerListenerData(std::function 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 { - public: - LoggerThread(std::function on_start, std::function on_exit, - int inst) - : CallbackThread(std::move(on_start), std::move(on_exit)), m_inst(inst) {} - - 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 callback, - const LogMessage& data) { - callback(data); - } - - int m_inst; -}; - -} // namespace impl - -class LoggerImpl : public wpi::CallbackManager { - friend class LoggerTest; - friend class wpi::CallbackManager; - +class LoggerImpl { public: explicit LoggerImpl(int inst); + ~LoggerImpl(); - void Start(); + NT_Logger Add(std::function callback, + unsigned int minLevel, unsigned int maxLevel); - unsigned int Add(std::function callback, - unsigned int min_level, unsigned int max_level); - unsigned int AddPolled(unsigned int poller_uid, unsigned int min_level, - unsigned int max_level); + NT_LoggerPoller CreatePoller(); + void DestroyPoller(NT_LoggerPoller pollerHandle); + NT_Logger AddPolled(NT_LoggerPoller pollerHandle, unsigned int minLevel, + unsigned int maxLevel); + std::vector ReadQueue(NT_LoggerPoller pollerHandle); + void Remove(NT_Logger listenerHandle); unsigned int GetMinLevel(); @@ -80,8 +40,37 @@ class LoggerImpl : public wpi::CallbackManager { private: int m_inst; + mutable wpi::mutex m_mutex; + + struct PollerData { + static constexpr auto kType = Handle::kLoggerPoller; + + explicit PollerData(NT_LoggerPoller handle) : handle{handle} {} + + wpi::SignalObject handle; + std::vector queue; + }; + HandleMap m_pollers; + + struct ListenerData { + static constexpr auto kType = Handle::kLogger; + + ListenerData(NT_Logger handle, PollerData* poller, unsigned int minLevel, + unsigned int maxLevel) + : handle{handle}, + poller{poller}, + minLevel{minLevel}, + maxLevel{maxLevel} {} + + wpi::SignalObject handle; + PollerData* poller; + unsigned int minLevel; + unsigned int maxLevel; + }; + HandleMap m_listeners; + + class Thread; + wpi::SafeThreadOwner m_thread; }; } // 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 6013f4e0e4..0000000000 --- a/ntcore/src/main/native/cpp/Message.cpp +++ /dev/null @@ -1,375 +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 - -#include "Log.h" -#include "WireDecoder.h" -#include "WireEncoder.h" - -#define kClearAllMagic 0xD06CB27Aul - -using namespace nt; - -std::shared_ptr 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(static_cast(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: {}", - static_cast(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::ClientHello(std::string_view self_id) { - auto msg = std::make_shared(kClientHello, private_init()); - msg->m_str = self_id; - return msg; -} - -std::shared_ptr Message::ServerHello(unsigned int flags, - std::string_view self_id) { - auto msg = std::make_shared(kServerHello, private_init()); - msg->m_str = self_id; - msg->m_flags = flags; - return msg; -} - -std::shared_ptr Message::EntryAssign(std::string_view name, - unsigned int id, - unsigned int seq_num, - std::shared_ptr value, - unsigned int flags) { - auto msg = std::make_shared(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::EntryUpdate(unsigned int id, - unsigned int seq_num, - std::shared_ptr value) { - auto msg = std::make_shared(kEntryUpdate, private_init()); - msg->m_value = value; - msg->m_id = id; - msg->m_seq_num_uid = seq_num; - return msg; -} - -std::shared_ptr Message::FlagsUpdate(unsigned int id, - unsigned int flags) { - auto msg = std::make_shared(kFlagsUpdate, private_init()); - msg->m_id = id; - msg->m_flags = flags; - return msg; -} - -std::shared_ptr Message::EntryDelete(unsigned int id) { - auto msg = std::make_shared(kEntryDelete, private_init()); - msg->m_id = id; - return msg; -} - -std::shared_ptr Message::ExecuteRpc(unsigned int id, unsigned int uid, - std::string_view params) { - auto msg = std::make_shared(kExecuteRpc, private_init()); - msg->m_str = params; - msg->m_id = id; - msg->m_seq_num_uid = uid; - return msg; -} - -std::shared_ptr Message::RpcResponse(unsigned int id, unsigned int uid, - std::string_view result) { - auto msg = std::make_shared(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 ec34a75fc3..0000000000 --- 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 -#include -#include -#include - -#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; - - 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() 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 Read(WireDecoder& decoder, - GetEntryTypeFunc get_entry_type); - - // Create messages without data - static std::shared_ptr KeepAlive() { - return std::make_shared(kKeepAlive, private_init()); - } - static std::shared_ptr ProtoUnsup() { - return std::make_shared(kProtoUnsup, private_init()); - } - static std::shared_ptr ServerHelloDone() { - return std::make_shared(kServerHelloDone, private_init()); - } - static std::shared_ptr ClientHelloDone() { - return std::make_shared(kClientHelloDone, private_init()); - } - static std::shared_ptr ClearEntries() { - return std::make_shared(kClearEntries, private_init()); - } - - // Create messages with data - static std::shared_ptr ClientHello(std::string_view self_id); - static std::shared_ptr ServerHello(unsigned int flags, - std::string_view self_id); - static std::shared_ptr EntryAssign(std::string_view name, - unsigned int id, - unsigned int seq_num, - std::shared_ptr value, - unsigned int flags); - static std::shared_ptr EntryUpdate(unsigned int id, - unsigned int seq_num, - std::shared_ptr value); - static std::shared_ptr FlagsUpdate(unsigned int id, - unsigned int flags); - static std::shared_ptr EntryDelete(unsigned int id); - static std::shared_ptr ExecuteRpc(unsigned int id, unsigned int uid, - std::string_view params); - static std::shared_ptr 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 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 0000000000..3295c2438a --- /dev/null +++ b/ntcore/src/main/native/cpp/NetworkClient.cpp @@ -0,0 +1,530 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(wpi::span> 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 m_parallelConnect; + std::shared_ptr m_readLocalTimer; + std::shared_ptr m_sendValuesTimer; + std::shared_ptr> m_flushLocal; + std::shared_ptr> m_flush; + + std::vector m_localMsgs; + + std::vector> m_servers; + + std::pair m_dsClientServer{"", 0}; + std::shared_ptr m_dsClient; + + // shared with user + std::atomic*> m_flushLocalAtomic{nullptr}; + std::atomic*> 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 m_wire; + std::shared_ptr m_clientImpl; +}; + +class NCImpl4 : public NCImpl { + public: + NCImpl4(int inst, std::string_view id, net::ILocalStorage& localStorage, + IConnectionList& connList, wpi::Logger& logger); + ~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::unique_ptr m_wire; + std::unique_ptr 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( + wpi::span> servers, + unsigned int defaultPort) { + std::vector> 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([=](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 + 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] { + 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); + 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(tcp); + auto clientImpl = std::make_shared( + 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{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()); + } + }); + + { + net3::ClientStartup3 startup{*m_clientImpl}; + m_localStorage.StartNetwork(startup); + } + m_localStorage.SetNetwork(&m_localQueue); + m_clientImpl->SetLocal(&m_localStorage); + }); + + tcp.SetData(clientImpl); + tcp.data.connect( + [clientImpl = clientImpl.get()](uv::Buffer& buf, size_t len) { + clientImpl->ProcessIncoming( + {reinterpret_cast(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) + : 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_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] { + 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); + 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; + auto ws = + wpi::WebSocket::CreateClient(tcp, fmt::format("/nt/{}", m_id), "", + {{"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_unique(ws); + m_clientImpl = std::make_unique( + m_loop.Now().count(), m_inst, *m_wire, m_logger, + [this](uint32_t repeatMs) { + DEBUG4("Setting periodic timer to {}", repeatMs); + m_sendValuesTimer->Start(uv::Timer::Time{repeatMs}, + uv::Timer::Time{repeatMs}); + }); + { + net::ClientStartup startup{*m_clientImpl}; + m_localStorage.StartNetwork(startup); + } + m_localStorage.SetNetwork(&m_localQueue); + m_clientImpl->SetLocal(&m_localStorage); + 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) { + m_clientImpl->ProcessIncomingText(data); + }); + ws.binary.connect([this](wpi::span data, bool) { + 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); +} + +class NetworkClient::Impl final : public NCImpl4 { + public: + Impl(int inst, std::string_view id, net::ILocalStorage& localStorage, + IConnectionList& connList, wpi::Logger& logger) + : NCImpl4{inst, id, localStorage, connList, logger} {} +}; + +NetworkClient::NetworkClient(int inst, std::string_view id, + net::ILocalStorage& localStorage, + IConnectionList& connList, wpi::Logger& logger) + : m_impl{std::make_unique(inst, id, localStorage, connList, logger)} { +} + +NetworkClient::~NetworkClient() { + m_impl->m_localStorage.ClearNetwork(); + m_impl->m_connList.ClearConnections(); +} + +void NetworkClient::SetServers( + wpi::span> 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(); + 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(inst, id, localStorage, connList, logger)} { +} + +NetworkClient3::~NetworkClient3() { + m_impl->m_localStorage.ClearNetwork(); + m_impl->m_connList.ClearConnections(); +} + +void NetworkClient3::SetServers( + wpi::span> 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 0000000000..d360c716e7 --- /dev/null +++ b/ntcore/src/main/native/cpp/NetworkClient.h @@ -0,0 +1,68 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include +#include +#include + +#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); + ~NetworkClient() final; + + void SetServers( + wpi::span> servers) final; + + void StartDSClient(unsigned int port) final; + void StopDSClient() final; + + void FlushLocal() final; + void Flush() final; + + private: + class Impl; + std::unique_ptr 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( + wpi::span> servers) final; + + void StartDSClient(unsigned int port) final; + void StopDSClient() final; + + void FlushLocal() final; + void Flush() final; + + private: + class Impl; + std::unique_ptr 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 a50b4befe5..0000000000 --- a/ntcore/src/main/native/cpp/NetworkConnection.cpp +++ /dev/null @@ -1,380 +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 - -#include -#include -#include - -#include "IConnectionNotifier.h" -#include "Log.h" -#include "WireDecoder.h" -#include "WireEncoder.h" - -using namespace nt; - -NetworkConnection::NetworkConnection(unsigned int uid, - std::unique_ptr 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(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>( - 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={}", - static_cast(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={}", - static_cast(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 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 59f18ff2d7..0000000000 --- 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 - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#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()> get_msg, - std::function>)> send_msgs)>; - using ProcessIncomingFunc = - std::function, NetworkConnection*)>; - using Outgoing = std::vector>; - using OutgoingQueue = wpi::ConcurrentQueue; - - NetworkConnection(unsigned int uid, - std::unique_ptr 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 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 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> 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 0000000000..1193228607 --- /dev/null +++ b/ntcore/src/main/native/cpp/NetworkServer.cpp @@ -0,0 +1,556 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 m_sendValuesTimer; +}; + +class ServerConnection4 final + : public ServerConnection, + public wpi::HttpWebSocketServerConnection { + public: + ServerConnection4(std::shared_ptr 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::unique_ptr m_wire; +}; + +class ServerConnection3 : public ServerConnection { + public: + ServerConnection3(std::shared_ptr stream, NSImpl& server, + std::string_view addr, unsigned int port, + wpi::Logger& logger); + + private: + std::unique_ptr 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 initDone); + + void HandleLocal(); + void LoadPersistent(); + 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 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 m_readLocalTimer; + std::shared_ptr m_savePersistentTimer; + std::shared_ptr> m_flushLocal; + std::shared_ptr> m_flush; + + std::vector m_localMsgs; + + net::ServerImpl m_serverImpl; + + // shared with user (must be atomic or mutex-protected) + std::atomic*> m_flushLocalAtomic{nullptr}; + std::atomic*> m_flushAtomic{nullptr}; + mutable wpi::mutex m_mutex; + struct Connection { + ServerConnection* conn; + int connHandle; + }; + std::vector 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", + "NetworkTables" + "

WebSockets must be used to access NetworkTables." + ""); + } 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); + + std::string_view name; + if (wpi::starts_with(path, "/nt/")) { + name = wpi::drop_front(path, 4); + } + if (name.empty()) { + INFO("invalid path '{}' (from {}), closing", path, m_connInfo); + m_websocket->Fail(404, fmt::format("invalid path '{}'", path)); + return; + } + + m_websocket->SetMaxMessageSize(kMaxMessageSize); + + m_websocket->open.connect([this, name = std::string{name}](std::string_view) { + m_wire = std::make_unique(*m_websocket); + // TODO: set local flag appropriately + m_clientId = m_server.m_serverImpl.AddClient( + name, m_connInfo, false, *m_wire, + [this](uint32_t repeatMs) { UpdatePeriodicTimer(repeatMs); }); + if (m_clientId < 0) { + INFO("duplicate connection name '{}' (from {}), closing", name, + m_connInfo); + m_websocket->Fail(409, fmt::format("duplicate name '{}'", name)); + return; + } + m_info.remote_id = name; + m_server.AddConnection(this, m_info); + m_websocket->closed.connect([this](uint16_t, std::string_view reason) { + INFO("NT4 connection '{}' closed (from {})", m_info.remote_id, + m_connInfo); + ConnectionClosed(); + }); + m_websocket->text.connect([this](std::string_view data, bool) { + m_server.m_serverImpl.ProcessIncomingText(m_clientId, data); + }); + m_websocket->binary.connect([this](wpi::span data, bool) { + m_server.m_serverImpl.ProcessIncomingBinary(m_clientId, data); + }); + + SetupPeriodicTimer(); + }); +} + +ServerConnection3::ServerConnection3(std::shared_ptr stream, + NSImpl& server, std::string_view addr, + unsigned int port, wpi::Logger& logger) + : ServerConnection{server, addr, port, logger}, + m_wire{std::make_unique(*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); + }, + [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("NT3 connection '{}' closed (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(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 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([=](uv::Loop& loop) { + // connect local storage to server + { + net::ServerStartup startup{m_serverImpl}; + m_localStorage.StartNetwork(startup); + } + m_localStorage.SetNetwork(&m_localQueue); + m_serverImpl.SetLocal(&m_localStorage); + + // load persistent file first, then initialize + uv::QueueWork( + m_loop, [this] { LoadPersistent(); }, [this] { Init(); }); + }); +} + +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; + } +} + +static void 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}; + 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() { + 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, + [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(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(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 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 initDone) + : m_impl{std::make_unique(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 0000000000..b70c968d61 --- /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 +#include +#include + +#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 initDone); + ~NetworkServer(); + + void FlushLocal(); + void Flush(); + + private: + class Impl; + std::unique_ptr m_impl; +}; + +} // namespace nt diff --git a/ntcore/src/main/native/cpp/PubSubOptions.cpp b/ntcore/src/main/native/cpp/PubSubOptions.cpp new file mode 100644 index 0000000000..d180bc3720 --- /dev/null +++ b/ntcore/src/main/native/cpp/PubSubOptions.cpp @@ -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. + +#include "PubSubOptions.h" + +#include "ntcore_cpp.h" + +using namespace nt; + +nt::PubSubOptions::PubSubOptions(wpi::span options) { + for (auto&& option : options) { + switch (option.type) { + case NT_PUBSUB_PERIODIC: + periodic = option.value; + break; + case NT_PUBSUB_SENDALL: + sendAll = option.value != 0; + if (sendAll) { + pollStorageSize = 20; + } + break; + case NT_PUBSUB_TOPICSONLY: + topicsOnly = option.value != 0; + break; + case NT_PUBSUB_KEEPDUPLICATES: + keepDuplicates = option.value != 0; + break; + case NT_PUBSUB_POLLSTORAGE: + pollStorageSize = static_cast(option.value); + break; + default: + break; + } + } +} diff --git a/ntcore/src/main/native/cpp/PubSubOptions.h b/ntcore/src/main/native/cpp/PubSubOptions.h new file mode 100644 index 0000000000..4df5364368 --- /dev/null +++ b/ntcore/src/main/native/cpp/PubSubOptions.h @@ -0,0 +1,27 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include "ntcore_cpp.h" + +namespace nt { + +// options built from array of PubSubOption +class PubSubOptions { + public: + PubSubOptions() = default; + explicit PubSubOptions(wpi::span options); + + double periodic = 0.1; + size_t pollStorageSize = 1; + bool sendAll = false; + bool topicsOnly = false; + bool prefixMatch = false; + bool keepDuplicates = false; +}; + +} // 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 b4bf96a006..0000000000 --- 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 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 b8ae6b7bad..0000000000 --- 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 - -#include -#include -#include - -#include "Handle.h" -#include "IRpcServer.h" -#include "Log.h" - -namespace nt { - -namespace impl { - -using RpcIdPair = std::pair; - -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>; - -class RpcServerThread - : public wpi::CallbackThread { - public: - RpcServerThread(std::function on_start, std::function 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 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 m_response_map; -}; - -} // namespace impl - -class RpcServer - : public IRpcServer, - public wpi::CallbackManager { - friend class RpcServerTest; - friend class wpi::CallbackManager; - - public: - RpcServer(int inst, wpi::Logger& logger); - - void Start(); - - unsigned int Add(std::function 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/Storage.cpp b/ntcore/src/main/native/cpp/Storage.cpp deleted file mode 100644 index 862f5b5454..0000000000 --- a/ntcore/src/main/native/cpp/Storage.cpp +++ /dev/null @@ -1,1514 +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 -#include -#include - -#include "Handle.h" -#include "IDispatcher.h" -#include "IEntryNotifier.h" -#include "INetworkConnection.h" -#include "IRpcServer.h" -#include "Log.h" -#include "ntcore_c.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 msg, - INetworkConnection* conn, - std::weak_ptr 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 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 - Notify(entry, NT_NOTIFY_NEW, false); - 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 - Notify(entry, notify_flags, 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; - 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 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 - Notify(entry, NT_NOTIFY_UPDATE, 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::ProcessIncomingFlagsUpdate(std::shared_ptr 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 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 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 msg, INetworkConnection* /*conn*/, - std::weak_ptr 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 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>* 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> msgs, - bool /*new_server*/, std::vector>* 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> 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 - Notify(entry, NT_NOTIFY_NEW, false); - } 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 - Notify(entry, notify_flags, false); - } - } - - // 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 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 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) { - 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) { - 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) { - 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) { - 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, - std::unique_lock& 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) { - Notify(entry, NT_NOTIFY_NEW, local); - } else if (*old_value != *value) { - Notify(entry, NT_NOTIFY_UPDATE, local); - } - - // 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) { - 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) { - 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& 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 - Notify(entry, NT_NOTIFY_FLAGS, local); - - // 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& 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 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 - Notify(entry, NT_NOTIFY_DELETE, local, old_value); - - // 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); - } -} - -static std::string_view GetStorageTypeStr(NT_Type type) { - switch (type) { - case NT_BOOLEAN: - return wpi::log::BooleanLogEntry::kDataType; - case NT_DOUBLE: - return wpi::log::DoubleLogEntry::kDataType; - case NT_STRING: - return wpi::log::StringLogEntry::kDataType; - case NT_RAW: - return wpi::log::RawLogEntry::kDataType; - case NT_BOOLEAN_ARRAY: - return wpi::log::BooleanArrayLogEntry::kDataType; - case NT_DOUBLE_ARRAY: - return wpi::log::DoubleArrayLogEntry::kDataType; - case NT_STRING_ARRAY: - return wpi::log::StringArrayLogEntry::kDataType; - default: - return {}; - } -} - -void Storage::Notify(Entry* entry, unsigned int flags, bool local, - std::shared_ptr value) { - auto& v = value ? value : entry->value; - - // notifications - m_notifier.NotifyEntry(entry->local_id, entry->name, v, - flags | (local ? NT_NOTIFY_LOCAL : 0)); - - if (m_dataloggers.empty()) { - return; - } - - // data logging - // fast path the common case - if (entry->datalogs.empty() && (flags & NT_NOTIFY_NEW) == 0) { - return; - } - - if (flags & NT_NOTIFY_DELETE) { - // remove all of the datalog entries - auto now = nt::Now(); - for (auto&& datalog : entry->datalogs) { - datalog.log->Finish(datalog.entry, now); - } - entry->datalogs.clear(); - entry->datalog_type = NT_UNASSIGNED; - return; - } - - if (!v) { - return; - } - - if (v->type() != entry->datalog_type) { - if (!entry->datalogs.empty()) { - // data type changed; need to finish any current logs - for (auto&& datalog : entry->datalogs) { - datalog.log->Finish(datalog.entry, v->time()); - } - entry->datalogs.clear(); - } - - // create matching loggers - auto type = GetStorageTypeStr(v->type()); - if (type.empty()) { - return; // not a type we're going to log - } - for (auto&& logger : m_dataloggers) { - if (wpi::starts_with(entry->name, logger.prefix)) { - entry->datalogs.emplace_back( - logger.log, - logger.log->Start( - fmt::format("{}{}", logger.log_prefix, - wpi::drop_front(entry->name, logger.prefix.size())), - type, "{\"source\":\"NT\"}", v->time()), - logger.uid); - } - } - - if (entry->datalogs.empty()) { - return; // we're done, nothing to log - } - - // set datalog_type - entry->datalog_type = v->type(); - } - - auto time = v->time(); - switch (v->type()) { - case NT_BOOLEAN: { - auto val = v->GetBoolean(); - for (auto&& datalog : entry->datalogs) { - datalog.log->AppendBoolean(datalog.entry, val, time); - } - break; - } - case NT_DOUBLE: { - auto val = v->GetDouble(); - for (auto&& datalog : entry->datalogs) { - datalog.log->AppendDouble(datalog.entry, val, time); - } - break; - } - case NT_STRING: { - auto val = v->GetString(); - for (auto&& datalog : entry->datalogs) { - datalog.log->AppendString(datalog.entry, val, time); - } - break; - } - case NT_RAW: { - auto val = v->GetRaw(); - for (auto&& datalog : entry->datalogs) { - datalog.log->AppendRaw( - datalog.entry, - {reinterpret_cast(val.data()), val.size()}, time); - } - break; - } - case NT_BOOLEAN_ARRAY: { - auto val = v->GetBooleanArray(); - for (auto&& datalog : entry->datalogs) { - datalog.log->AppendBooleanArray(datalog.entry, val, time); - } - break; - } - case NT_DOUBLE_ARRAY: { - auto val = v->GetDoubleArray(); - for (auto&& datalog : entry->datalogs) { - datalog.log->AppendDoubleArray(datalog.entry, val, time); - } - break; - } - case NT_STRING_ARRAY: { - auto val = v->GetStringArray(); - for (auto&& datalog : entry->datalogs) { - datalog.log->AppendStringArray(datalog.entry, val, time); - } - break; - } - case NT_UNASSIGNED: - case NT_RPC: - break; - } -} - -template -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 - Notify(entry, NT_NOTIFY_DELETE, local); - // 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 Storage::GetEntries(std::string_view prefix, - unsigned int types) { - std::scoped_lock lock(m_mutex); - std::vector 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 Storage::GetEntryInfo(int inst, std::string_view prefix, - unsigned int types) { - std::scoped_lock lock(m_mutex); - std::vector 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 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 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; -} - -unsigned int Storage::StartDataLog(wpi::log::DataLog& log, - std::string_view prefix, - std::string_view log_prefix) { - std::scoped_lock lock(m_mutex); - - // create - unsigned int uid = m_dataloggers.emplace_back(log, prefix, log_prefix); - m_dataloggers[uid].uid = uid; - - // start logging any matching entries - auto now = nt::Now(); - for (auto&& entry : m_entries) { - if (!entry.second || !wpi::starts_with(entry.second->name, prefix) || - !entry.second->value) { - continue; - } - auto type = GetStorageTypeStr(entry.second->value->type()); - if (type.empty()) { - continue; // not a type we're going to log - } - int logentry = log.Start( - fmt::format("{}{}", log_prefix, - wpi::drop_front(entry.second->name, prefix.size())), - type, "{\"source\":\"NT\"}", now); - entry.second->datalogs.emplace_back(&log, logentry, uid); - // log current value - auto& v = *entry.second->value; - entry.second->datalog_type = v.type(); - auto time = v.time(); - switch (v.type()) { - case NT_BOOLEAN: - log.AppendBoolean(logentry, v.GetBoolean(), time); - break; - case NT_DOUBLE: - log.AppendDouble(logentry, v.GetDouble(), time); - break; - case NT_STRING: - log.AppendString(logentry, v.GetString(), time); - break; - case NT_RAW: { - auto val = v.GetRaw(); - log.AppendRaw( - logentry, - {reinterpret_cast(val.data()), val.size()}, time); - break; - } - case NT_BOOLEAN_ARRAY: - log.AppendBooleanArray(logentry, v.GetBooleanArray(), time); - break; - case NT_DOUBLE_ARRAY: - log.AppendDoubleArray(logentry, v.GetDoubleArray(), time); - break; - case NT_STRING_ARRAY: - log.AppendStringArray(logentry, v.GetStringArray(), time); - break; - default: - break; - } - } - - return uid; -} - -void Storage::StopDataLog(unsigned int uid) { - std::scoped_lock lock(m_mutex); - - // erase the datalogger - auto datalogger = m_dataloggers.erase(uid); - if (!datalogger) { - return; - } - - // finish any active entries - auto now = nt::Now(); - for (auto&& entry : m_entries) { - if (!entry.second || entry.second->datalogs.empty()) { - continue; - } - auto it = std::find_if( - entry.second->datalogs.begin(), entry.second->datalogs.end(), - [&](const auto& elem) { return elem.logger_uid == uid; }); - if (it != entry.second->datalogs.end()) { - it->log->Finish(it->entry, now); - entry.second->datalogs.erase(it); - } - } -} - -bool Storage::GetPersistentEntries( - bool periodic, - std::vector>>* 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>& a, - const std::pair>& b) { - return a.first < b.first; - }); - return true; -} - -bool Storage::GetEntries( - std::string_view prefix, - std::vector>>* 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>& a, - const std::pair>& 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(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 fa36b724aa..0000000000 --- a/ntcore/src/main/native/cpp/Storage.h +++ /dev/null @@ -1,304 +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 - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#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 msg, INetworkConnection* conn, - std::weak_ptr conn_weak) override; - void GetInitialAssignments( - INetworkConnection& conn, - std::vector>* msgs) override; - void ApplyInitialAssignments( - INetworkConnection& conn, wpi::span> msgs, - bool new_server, - std::vector>* out_msgs) override; - - // User functions. These are the actual implementations of the corresponding - // user API functions in ntcore_cpp. - std::shared_ptr GetEntryValue(std::string_view name) const; - std::shared_ptr GetEntryValue(unsigned int local_id) const; - - bool SetDefaultEntryValue(std::string_view name, - std::shared_ptr value); - bool SetDefaultEntryValue(unsigned int local_id, - std::shared_ptr value); - - bool SetEntryValue(std::string_view name, std::shared_ptr value); - bool SetEntryValue(unsigned int local_id, std::shared_ptr value); - - void SetEntryTypeValue(std::string_view name, std::shared_ptr value); - void SetEntryTypeValue(unsigned int local_id, std::shared_ptr 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 GetEntryInfo(int inst, std::string_view prefix, - unsigned int types); - - unsigned int AddListener( - std::string_view prefix, - std::function callback, - unsigned int flags) const; - unsigned int AddListener( - unsigned int local_id, - std::function 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; - - unsigned int StartDataLog(wpi::log::DataLog& log, std::string_view prefix, - std::string_view log_prefix); - void StopDataLog(unsigned int uid); - - // Index-only - unsigned int GetEntry(std::string_view name); - std::vector 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 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 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 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: - struct DataLoggerEntry { - DataLoggerEntry(wpi::log::DataLog* log, int entry, unsigned int logger_uid) - : log{log}, entry{entry}, logger_uid{logger_uid} {} - - wpi::log::DataLog* log; - int entry; - unsigned int logger_uid; - }; - - struct DataLogger { - DataLogger() = default; - DataLogger(wpi::log::DataLog& log, std::string_view prefix, - std::string_view log_prefix) - : log{&log}, prefix{prefix}, log_prefix{log_prefix} {} - - explicit operator bool() const { return log != nullptr; } - - wpi::log::DataLog* log = nullptr; - std::string prefix; - std::string log_prefix; - unsigned int uid; - }; - - // 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; - 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}; - - // log entries - wpi::SmallVector datalogs; - NT_Type datalog_type{NT_UNASSIGNED}; - }; - - using EntriesMap = wpi::StringMap; - using IdMap = std::vector; - using LocalMap = std::vector>; - using RpcIdPair = std::pair; - using RpcResultMap = wpi::DenseMap; - using RpcBlockingCallSet = wpi::SmallSet; - - mutable wpi::mutex m_mutex; - EntriesMap m_entries; - IdMap m_idmap; - LocalMap m_localmap; - RpcResultMap m_rpc_results; - RpcBlockingCallSet m_rpc_blocking_calls; - wpi::UidVector m_dataloggers; - // 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 msg, - INetworkConnection* conn); - void ProcessIncomingEntryUpdate(std::shared_ptr msg, - INetworkConnection* conn); - void ProcessIncomingFlagsUpdate(std::shared_ptr msg, - INetworkConnection* conn); - void ProcessIncomingEntryDelete(std::shared_ptr msg, - INetworkConnection* conn); - void ProcessIncomingClearEntries(std::shared_ptr msg, - INetworkConnection* conn); - void ProcessIncomingExecuteRpc(std::shared_ptr msg, - INetworkConnection* conn, - std::weak_ptr conn_weak); - void ProcessIncomingRpcResponse(std::shared_ptr msg, - INetworkConnection* conn); - - bool GetPersistentEntries( - bool periodic, - std::vector>>* entries) - const; - bool GetEntries(std::string_view prefix, - std::vector>>* - entries) const; - void SetEntryValueImpl(Entry* entry, std::shared_ptr value, - std::unique_lock& lock, bool local); - void SetEntryFlagsImpl(Entry* entry, unsigned int flags, - std::unique_lock& lock, bool local); - void DeleteEntryImpl(Entry* entry, std::unique_lock& lock, - bool local); - - void Notify(Entry* entry, unsigned int flags, bool local, - std::shared_ptr value = {}); - - // Must be called with m_mutex held - template - 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 98046e7677..0000000000 --- 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 -#include -#include - -#include -#include -#include -#include - -#include "IDispatcher.h" -#include "IEntryNotifier.h" -#include "Storage.h" - -using namespace nt; - -namespace { - -class LoadPersistentImpl { - public: - using Entry = std::pair>; - using WarnFunc = std::function; - - LoadPersistentImpl(wpi::raw_istream& is, WarnFunc warn) - : m_is(is), m_warn(std::move(warn)) {} - - bool Load(std::string_view prefix, std::vector* entries); - - private: - bool ReadLine(); - bool ReadHeader(); - NT_Type ReadType(); - std::string_view ReadName(wpi::SmallVectorImpl& buf); - std::shared_ptr ReadValue(NT_Type type); - std::shared_ptr ReadBooleanValue(); - std::shared_ptr ReadDoubleValue(); - std::shared_ptr ReadStringValue(); - std::shared_ptr ReadRawValue(); - std::shared_ptr ReadBooleanArrayValue(); - std::shared_ptr ReadDoubleArrayValue(); - std::shared_ptr 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 m_buf_boolean_array; - std::vector m_buf_double_array; - std::vector 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 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& 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(ch)); - break; - } - default: - buf.push_back(*s); - break; - } - } - return {buf.data(), buf.size()}; -} - -bool LoadPersistentImpl::Load(std::string_view prefix, - std::vector* 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& 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 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 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 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 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 LoadPersistentImpl::ReadRawValue() { - wpi::SmallString<128> buf; - size_t nr; - return Value::MakeRaw(wpi::Base64Decode(m_line, &nr, buf)); -} - -std::shared_ptr 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 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 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 warn) { - // entries to add - std::vector 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> 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 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 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 6d5db9f83a..0000000000 --- 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 -#include - -#include -#include -#include -#include -#include -#include - -#include "Log.h" -#include "Storage.h" - -using namespace nt; - -namespace { - -class SavePersistentImpl { - public: - using Entry = std::pair>; - - explicit SavePersistentImpl(wpi::raw_ostream& os) : m_os(os) {} - - void Save(wpi::span entries); - - private: - void WriteString(std::string_view str); - void WriteHeader(); - void WriteEntries(wpi::span 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 entries) { - WriteHeader(); - WriteEntries(entries); -} - -void SavePersistentImpl::WriteHeader() { - m_os << "[NetworkTables Storage 3.0]\n"; -} - -void SavePersistentImpl::WriteEntries(wpi::span 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 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 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 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 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 0000000000..d6de540267 --- /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 0000000000..80c7f8e159 --- /dev/null +++ b/ntcore/src/main/native/cpp/Types_internal.h @@ -0,0 +1,17 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#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); + +} // namespace nt diff --git a/ntcore/src/main/native/cpp/Value.cpp b/ntcore/src/main/native/cpp/Value.cpp index e0d65c998b..e30c085cfb 100644 --- a/ntcore/src/main/native/cpp/Value.cpp +++ b/ntcore/src/main/native/cpp/Value.cpp @@ -11,23 +11,56 @@ #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(wpi::span value) + : strings{value.begin(), value.end()} { + InitNtStrings(); + } + explicit StringArrayStorage(std::vector&& value) + : strings{std::move(value)} { + InitNtStrings(); + } + void InitNtStrings(); + + std::vector strings; + std::vector 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(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 +68,85 @@ Value::Value(NT_Type type, uint64_t time, const private_init&) { } } -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::MakeBooleanArray(wpi::span value, - uint64_t time) { - auto val = std::make_shared(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(wpi::span value, int64_t time) { + Value val{NT_BOOLEAN_ARRAY, time, private_init{}}; + auto data = std::make_shared>(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::MakeBooleanArray(wpi::span value, - uint64_t time) { - auto val = std::make_shared(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(wpi::span value, int64_t time) { + Value val{NT_BOOLEAN_ARRAY, time, private_init{}}; + auto data = std::make_shared>(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::MakeDoubleArray(wpi::span value, - uint64_t time) { - auto val = std::make_shared(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::MakeIntegerArray(wpi::span value, int64_t time) { + Value val{NT_INTEGER_ARRAY, time, private_init{}}; + auto data = + std::make_shared>(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::MakeStringArray( - wpi::span value, uint64_t time) { - auto val = std::make_shared(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(value[i].c_str()); - val->m_val.data.arr_string.arr[i].len = value[i].size(); - } +Value Value::MakeFloatArray(wpi::span value, int64_t time) { + Value val{NT_FLOAT_ARRAY, time, private_init{}}; + auto data = std::make_shared>(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; } -std::shared_ptr Value::MakeStringArray(std::vector&& value, - uint64_t time) { - auto val = std::make_shared(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(val->m_string_array[i].c_str()); - val->m_val.data.arr_string.arr[i].len = val->m_string_array[i].size(); - } +Value Value::MakeDoubleArray(wpi::span value, int64_t time) { + Value val{NT_DOUBLE_ARRAY, time, private_init{}}; + auto data = std::make_shared>(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::MakeStringArray(wpi::span value, int64_t time) { + Value val{NT_STRING_ARRAY, time, private_init{}}; + auto data = std::make_shared(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&& value, int64_t time) { + Value val{NT_STRING_ARRAY, time, private_init{}}; + auto data = std::make_shared(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); - break; - case NT_RPC: - ConvertToC(in.GetRpc(), &out->data.v_raw); + case NT_RAW: { + auto v = in.GetRaw(); + out->data.v_raw.data = static_cast(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_BOOLEAN_ARRAY: { auto v = in.GetBooleanArray(); out->data.arr_boolean.arr = @@ -130,6 +155,22 @@ void nt::ConvertToC(const Value& in, NT_Value* out) { 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(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(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 +184,21 @@ void nt::ConvertToC(const Value& in, NT_Value* out) { out->data.arr_string.arr = static_cast( 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(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 +208,48 @@ void nt::ConvertToC(std::string_view in, NT_String* out) { out->str[in.size()] = '\0'; } -std::shared_ptr 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)); + wpi::span(value.data.arr_boolean.arr, value.data.arr_boolean.size), + value.last_change); + case NT_INTEGER_ARRAY: + return Value::MakeIntegerArray( + wpi::span(value.data.arr_int.arr, value.data.arr_int.size), + value.last_change); + case NT_FLOAT_ARRAY: + return Value::MakeFloatArray( + wpi::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)); + wpi::span(value.data.arr_double.arr, value.data.arr_double.size), + value.last_change); case NT_STRING_ARRAY: { std::vector 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 +262,20 @@ bool nt::operator==(const Value& lhs, const Value& rhs) { 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 +284,21 @@ bool nt::operator==(const Value& lhs, const Value& rhs) { 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 +308,8 @@ bool nt::operator==(const Value& lhs, const Value& rhs) { 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(lhs.m_storage.get())->strings == + static_cast(rhs.m_storage.get())->strings; default: // assert(false && "unknown value type"); return false; diff --git a/ntcore/src/main/native/cpp/Value_internal.h b/ntcore/src/main/native/cpp/Value_internal.h index 54850ab2c1..3b56768764 100644 --- a/ntcore/src/main/native/cpp/Value_internal.h +++ b/ntcore/src/main/native/cpp/Value_internal.h @@ -2,12 +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_VALUE_INTERNAL_H_ -#define NTCORE_VALUE_INTERNAL_H_ +#pragma once +#include #include #include #include +#include + +#include #include "ntcore_c.h" @@ -15,13 +18,42 @@ namespace nt { class Value; +template +inline void ConvertToC(const T& in, T* out) { + *out = in; +} + void ConvertToC(const Value& in, NT_Value* out); -std::shared_ptr 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 +O* ConvertToC(const std::vector& in, size_t* out_len) { + if (!out_len) { + return nullptr; + } + *out_len = in.size(); + if (in.empty()) { + return nullptr; + } + O* out = static_cast(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 +O* ConvertToC(const std::basic_string& in, size_t* out_len) { + char* out = static_cast(wpi::safe_malloc(in.size() + 1)); + std::memmove(out, in.data(), in.size()); // NOLINT + out[in.size()] = '\0'; + *out_len = in.size(); + return out; +} + +} // 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 c0647dc048..0000000000 --- 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 - -#include -#include -#include - -#include -#include -#include - -using namespace nt; - -static double ReadDouble(const char*& buf) { - // Fast but non-portable! - uint64_t val = (*reinterpret_cast(buf)) & 0xff; - ++buf; - val <<= 8; - val |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - val <<= 8; - val |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - val <<= 8; - val |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - val <<= 8; - val |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - val <<= 8; - val |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - val <<= 8; - val |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - val <<= 8; - val |= (*reinterpret_cast(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(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(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 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 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 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 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 34bfb0fdc8..0000000000 --- a/ntcore/src/main/native/cpp/WireDecoder.h +++ /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. - -#ifndef NTCORE_WIREDECODER_H_ -#define NTCORE_WIREDECODER_H_ - -#include - -#include -#include -#include - -#include -#include - -#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((*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(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(buf)) & 0xff; - ++buf; - v <<= 8; - v |= (*reinterpret_cast(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(buf)) & 0xff; - ++buf; - v <<= 8; - v |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - v <<= 8; - v |= (*reinterpret_cast(buf)) & 0xff; - ++buf; - v <<= 8; - v |= (*reinterpret_cast(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 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 b35b7808f0..0000000000 --- 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 - -#include -#include -#include - -#include -#include - -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((v >> 56) & 0xff), static_cast((v >> 48) & 0xff), - static_cast((v >> 40) & 0xff), static_cast((v >> 32) & 0xff), - static_cast((v >> 24) & 0xff), static_cast((v >> 16) & 0xff), - static_cast((v >> 8) & 0xff), static_cast(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 84edc39ebd..0000000000 --- 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 - -#include -#include -#include - -#include - -#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(val & 0xff)); - } - - /* Writes a 16-bit word. */ - void Write16(unsigned int val) { - m_data.append( - {static_cast((val >> 8) & 0xff), static_cast(val & 0xff)}); - } - - /* Writes a 32-bit word. */ - void Write32(uint32_t val) { - m_data.append({static_cast((val >> 24) & 0xff), - static_cast((val >> 16) & 0xff), - static_cast((val >> 8) & 0xff), - static_cast(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 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 b83c0c3e1d..c7786f9127 100644 --- a/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp +++ b/ntcore/src/main/native/cpp/jni/NetworkTablesJNI.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "edu_wpi_first_networktables_NetworkTablesJNI.h" #include "ntcore.h" @@ -20,6 +21,11 @@ using namespace wpi::java; #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 // @@ -30,15 +36,16 @@ static JClass booleanCls; static JClass connectionInfoCls; static JClass connectionNotificationCls; static JClass doubleCls; -static JClass entryInfoCls; -static JClass entryNotificationCls; +static JClass floatCls; static JClass logMessageCls; -static JClass rpcAnswerCls; +static JClass longCls; +static JClass topicInfoCls; +static JClass topicNotificationCls; static JClass valueCls; +static JClass valueNotificationCls; static JException illegalArgEx; static JException interruptedEx; static JException nullPointerEx; -static JException persistentEx; static const JClassInit classes[] = { {"java/lang/Boolean", &booleanCls}, @@ -46,17 +53,18 @@ static const JClassInit classes[] = { {"edu/wpi/first/networktables/ConnectionNotification", &connectionNotificationCls}, {"java/lang/Double", &doubleCls}, - {"edu/wpi/first/networktables/EntryInfo", &entryInfoCls}, - {"edu/wpi/first/networktables/EntryNotification", &entryNotificationCls}, + {"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}, + {"edu/wpi/first/networktables/TopicInfo", &topicInfoCls}, + {"edu/wpi/first/networktables/TopicNotification", &topicNotificationCls}, + {"edu/wpi/first/networktables/NetworkTableValue", &valueCls}, + {"edu/wpi/first/networktables/ValueNotification", &valueNotificationCls}}; 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" { @@ -83,6 +91,10 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { } } + if (!nt::JNI_LoadTypes(env)) { + return JNI_ERR; + } + return JNI_VERSION_1_6; } @@ -98,6 +110,7 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) { for (auto& c : exceptions) { c.cls->free(env); } + nt::JNI_UnloadTypes(env); jvm = nullptr; } @@ -107,72 +120,20 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) { // Conversions from Java objects to C++ // -inline std::shared_ptr FromJavaRaw(JNIEnv* env, jbyteArray jarr, - jlong time) { - CriticalJByteArrayRef ref{env, jarr}; - if (!ref) { - return nullptr; +static wpi::span FromJavaPubSubOptions( + JNIEnv* env, jintArray optionTypes, jdoubleArray optionValues, + wpi::SmallVectorImpl& arr) { + JIntArrayRef types{env, optionTypes}; + JDoubleArrayRef values{env, optionValues}; + if (types.size() != values.size()) { + return {}; } - return nt::Value::MakeRaw(ref.str(), time); -} - -inline std::shared_ptr FromJavaRawBB(JNIEnv* env, jobject jbb, - int len, jlong time) { - JByteArrayRef ref{env, jbb, len}; - if (!ref) { - return nullptr; + arr.clear(); + arr.reserve(types.size()); + for (size_t i = 0, iend = types.size(); i != iend; ++i) { + arr.emplace_back(static_cast(types[i]), values[i]); } - return nt::Value::MakeRaw(ref.str(), time); -} - -inline std::shared_ptr FromJavaRpc(JNIEnv* env, jbyteArray jarr, - jlong time) { - CriticalJByteArrayRef ref{env, jarr}; - if (!ref) { - return nullptr; - } - return nt::Value::MakeRpc(ref.str(), time); -} - -std::shared_ptr FromJavaBooleanArray(JNIEnv* env, jbooleanArray jarr, - jlong time) { - CriticalJBooleanArrayRef ref{env, jarr}; - if (!ref) { - return nullptr; - } - wpi::span elements{ref}; - size_t len = elements.size(); - std::vector arr; - arr.reserve(len); - for (size_t i = 0; i < len; ++i) { - arr.push_back(elements[i]); - } - return nt::Value::MakeBooleanArray(arr, time); -} - -std::shared_ptr FromJavaDoubleArray(JNIEnv* env, jdoubleArray jarr, - jlong time) { - CriticalJDoubleArrayRef ref{env, jarr}; - if (!ref) { - return nullptr; - } - return nt::Value::MakeDoubleArray(ref, time); -} - -std::shared_ptr FromJavaStringArray(JNIEnv* env, jobjectArray jarr, - jlong time) { - size_t len = env->GetArrayLength(jarr); - std::vector arr; - arr.reserve(len); - for (size_t i = 0; i < len; ++i) { - JLocal elem{ - env, static_cast(env->GetObjectArrayElement(jarr, i))}; - if (!elem) { - return nullptr; - } - arr.emplace_back(JStringRef{env, elem}.str()); - } - return nt::Value::MakeStringArray(std::move(arr), time); + return arr; } // @@ -182,54 +143,65 @@ std::shared_ptr FromJavaStringArray(JNIEnv* env, jobjectArray jarr, 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, "", "(Z)V"); } if (!doubleConstructor) { doubleConstructor = env->GetMethodID(doubleCls, "", "(D)V"); } + if (!floatConstructor) { + floatConstructor = env->GetMethodID(floatCls, "", "(F)V"); + } + if (!longConstructor) { + longConstructor = env->GetMethodID(longCls, "", "(J)V"); + } switch (value.type()) { case NT_BOOLEAN: return env->NewObject(booleanCls, booleanConstructor, static_cast(value.GetBoolean() ? 1 : 0)); + case NT_INTEGER: + return env->NewObject(longCls, longConstructor, + static_cast(value.GetInteger())); + case NT_FLOAT: + return env->NewObject(floatCls, floatConstructor, + static_cast(value.GetFloat())); case NT_DOUBLE: return env->NewObject(doubleCls, doubleConstructor, static_cast(value.GetDouble())); case NT_STRING: return MakeJString(env, value.GetString()); - case NT_RAW: { - auto data = value.GetRaw(); - return MakeJByteArray( - env, {reinterpret_cast(data.data()), data.size()}); - } + case NT_RAW: + 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: { - auto data = value.GetRpc(); - return MakeJByteArray( - env, {reinterpret_cast(data.data()), data.size()}); - } 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, "", "(ILjava/lang/Object;J)V"); + env->GetMethodID(valueCls, "", "(ILjava/lang/Object;JJ)V"); if (!value) { return env->NewObject(valueCls, constructor, static_cast(NT_UNASSIGNED), nullptr, - static_cast(0)); + static_cast(0), static_cast(0)); } - return env->NewObject(valueCls, constructor, static_cast(value->type()), - MakeJObject(env, *value), - static_cast(value->time())); + return env->NewObject(valueCls, constructor, static_cast(value.type()), + MakeJObject(env, value), + static_cast(value.time()), + static_cast(value.server_time())); } static jobject MakeJObject(JNIEnv* env, const nt::ConnectionInfo& info) { @@ -257,33 +229,6 @@ static jobject MakeJObject(JNIEnv* env, jobject inst, conn.obj()); } -static jobject MakeJObject(JNIEnv* env, jobject inst, - const nt::EntryInfo& info) { - static jmethodID constructor = - env->GetMethodID(entryInfoCls, "", - "(Ledu/wpi/first/networktables/" - "NetworkTableInstance;ILjava/lang/String;IIJ)V"); - JLocal name{env, MakeJString(env, info.name)}; - return env->NewObject( - entryInfoCls, constructor, inst, static_cast(info.entry), - name.obj(), static_cast(info.type), static_cast(info.flags), - static_cast(info.last_change)); -} - -static jobject MakeJObject(JNIEnv* env, jobject inst, - const nt::EntryNotification& notification) { - static jmethodID constructor = env->GetMethodID( - entryNotificationCls, "", - "(Ledu/wpi/first/networktables/NetworkTableInstance;IILjava/lang/" - "String;Ledu/wpi/first/networktables/NetworkTableValue;I)V"); - JLocal name{env, MakeJString(env, notification.name)}; - JLocal value{env, MakeJValue(env, notification.value.get())}; - return env->NewObject(entryNotificationCls, constructor, inst, - static_cast(notification.listener), - static_cast(notification.entry), name.obj(), - value.obj(), static_cast(notification.flags)); -} - static jobject MakeJObject(JNIEnv* env, jobject inst, const nt::LogMessage& msg) { static jmethodID constructor = env->GetMethodID( @@ -299,21 +244,53 @@ static jobject MakeJObject(JNIEnv* env, jobject inst, } static jobject MakeJObject(JNIEnv* env, jobject inst, - const nt::RpcAnswer& answer) { + const nt::TopicInfo& info) { + static jmethodID constructor = env->GetMethodID( + topicInfoCls, "", + "(Ledu/wpi/first/networktables/" + "NetworkTableInstance;ILjava/lang/String;ILjava/lang/String;)V"); + JLocal name{env, MakeJString(env, info.name)}; + JLocal typeStr{env, MakeJString(env, info.type_str)}; + return env->NewObject(topicInfoCls, constructor, inst, + static_cast(info.topic), name.obj(), + static_cast(info.type), typeStr.obj()); +} + +static jobject MakeJObject(JNIEnv* env, jobject inst, + const nt::TopicNotification& notification) { static jmethodID constructor = - env->GetMethodID(rpcAnswerCls, "", - "(Ledu/wpi/first/networktables/" - "NetworkTableInstance;IILjava/lang/String;[B" - "Ledu/wpi/first/networktables/ConnectionInfo;)V"); - JLocal name{env, MakeJString(env, answer.name)}; - JLocal params{ - env, MakeJByteArray( - env, {reinterpret_cast(answer.params.data()), - answer.params.size()})}; - JLocal conn{env, MakeJObject(env, answer.conn)}; - return env->NewObject( - rpcAnswerCls, constructor, inst, static_cast(answer.entry), - static_cast(answer.call), name.obj(), params.obj(), conn.obj()); + env->GetMethodID(topicNotificationCls, "", + "(ILedu/wpi/first/networktables/TopicInfo;I)V"); + JLocal info{env, MakeJObject(env, inst, notification.info)}; + return env->NewObject(topicNotificationCls, constructor, + static_cast(notification.listener), info.obj(), + static_cast(notification.flags)); +} + +static jobject MakeJObject(JNIEnv* env, jobject inst, + const nt::ValueNotification& notification) { + static jmethodID constructor = + env->GetMethodID(valueNotificationCls, "", + "(Ledu/wpi/first/networktables/NetworkTableInstance;III" + "Ledu/wpi/first/networktables/NetworkTableValue;I)V"); + JLocal value{env, MakeJValue(env, notification.value)}; + return env->NewObject(valueNotificationCls, constructor, inst, + static_cast(notification.listener), + static_cast(notification.topic), + static_cast(notification.subentry), value.obj(), + static_cast(notification.flags)); +} + +static jobjectArray MakeJObject(JNIEnv* env, wpi::span arr) { + jobjectArray jarr = env->NewObjectArray(arr.size(), valueCls, nullptr); + if (!jarr) { + return nullptr; + } + for (size_t i = 0; i < arr.size(); ++i) { + JLocal elem{env, MakeJValue(env, arr[i])}; + env->SetObjectArrayElement(jarr, i, elem.obj()); + } + return jarr; } static jobjectArray MakeJObject( @@ -331,20 +308,6 @@ static jobjectArray MakeJObject( return jarr; } -static jobjectArray MakeJObject(JNIEnv* env, jobject inst, - wpi::span arr) { - jobjectArray jarr = - env->NewObjectArray(arr.size(), entryNotificationCls, nullptr); - if (!jarr) { - return nullptr; - } - for (size_t i = 0; i < arr.size(); ++i) { - JLocal elem{env, MakeJObject(env, inst, arr[i])}; - env->SetObjectArrayElement(jarr, i, elem.obj()); - } - return jarr; -} - static jobjectArray MakeJObject(JNIEnv* env, jobject inst, wpi::span arr) { jobjectArray jarr = env->NewObjectArray(arr.size(), logMessageCls, nullptr); @@ -359,8 +322,23 @@ static jobjectArray MakeJObject(JNIEnv* env, jobject inst, } static jobjectArray MakeJObject(JNIEnv* env, jobject inst, - wpi::span arr) { - jobjectArray jarr = env->NewObjectArray(arr.size(), rpcAnswerCls, nullptr); + wpi::span arr) { + jobjectArray jarr = + env->NewObjectArray(arr.size(), topicNotificationCls, nullptr); + if (!jarr) { + return nullptr; + } + for (size_t i = 0; i < arr.size(); ++i) { + JLocal elem{env, MakeJObject(env, inst, arr[i])}; + env->SetObjectArrayElement(jarr, i, elem.obj()); + } + return jarr; +} + +static jobjectArray MakeJObject(JNIEnv* env, jobject inst, + wpi::span arr) { + jobjectArray jarr = + env->NewObjectArray(arr.size(), valueNotificationCls, nullptr); if (!jarr) { return nullptr; } @@ -427,31 +405,14 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_getInstanceFromHandle * Signature: (ILjava/lang/String;)I */ JNIEXPORT jint JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntry +Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntry__ILjava_lang_String_2 (JNIEnv* env, jclass, jint inst, jstring key) { if (!key) { 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}); } /* @@ -492,183 +453,514 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_getType /* * 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 typeStrData; + std::vector typeStrs; + typeStrs.reserve(len); + for (int i = 0; i < len; ++i) { + JLocal elem{ + env, static_cast(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 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 typeStrData; + std::vector typeStrs; + typeStrs.reserve(len); + for (int i = 0; i < len; ++i) { + JLocal elem{ + env, static_cast(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 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;[I[D)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_subscribe + (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr, + jintArray optionTypes, jdoubleArray optionValues) +{ + wpi::SmallVector options; + return nt::Subscribe( + topic, static_cast(type), JStringRef{env, typeStr}, + FromJavaPubSubOptions(env, optionTypes, optionValues, 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;[I[D)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_publish + (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr, + jintArray optionTypes, jdoubleArray optionValues) +{ + wpi::SmallVector options; + return nt::Publish( + topic, static_cast(type), JStringRef{env, typeStr}, + FromJavaPubSubOptions(env, optionTypes, optionValues, options)); +} + +/* + * Class: edu_wpi_first_networktables_NetworkTablesJNI + * Method: publishEx + * Signature: (IILjava/lang/String;Ljava/lang/String;[I[D)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_publishEx + (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr, + jstring properties, jintArray optionTypes, jdoubleArray optionValues) +{ + 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; + } + wpi::SmallVector options; + return nt::PublishEx( + topic, static_cast(type), JStringRef{env, typeStr}, j, + FromJavaPubSubOptions(env, optionTypes, optionValues, 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: getEntry + * Signature: (IILjava/lang/String;[I[D)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntry__IILjava_lang_String_2_3I_3D + (JNIEnv* env, jclass, jint topic, jint type, jstring typeStr, + jintArray optionTypes, jdoubleArray optionValues) +{ + wpi::SmallVector options; + return nt::GetEntry( + topic, static_cast(type), JStringRef{env, typeStr}, + FromJavaPubSubOptions(env, optionTypes, optionValues, 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;[I[D)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_subscribeMultiple + (JNIEnv* env, jclass, jint inst, jobjectArray prefixes, jintArray optionTypes, + jdoubleArray optionValues) +{ + if (!prefixes) { + nullPointerEx.Throw(env, "prefixes cannot be null"); + return {}; + } + int len = env->GetArrayLength(prefixes); + + std::vector prefixStrings; + std::vector prefixStringViews; + prefixStrings.reserve(len); + prefixStringViews.reserve(len); + for (int i = 0; i < len; ++i) { + JLocal elem{ + env, static_cast(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()); + } + + wpi::SmallVector options; + return nt::SubscribeMultiple( + inst, prefixStringViews, + FromJavaPubSubOptions(env, optionTypes, optionValues, 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)); } /* @@ -680,233 +972,7 @@ JNIEXPORT jobject JNICALL 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; - } - auto data = val->GetRaw(); - return MakeJByteArray( - env, {reinterpret_cast(data.data()), data.size()}); -} - -/* - * 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)); } /* @@ -935,188 +1001,168 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_getEntryFlags /* * 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 jelem{env, MakeJObject(env, instObject, arr[i])}; - env->SetObjectArrayElement(jarr, i, jelem); - } - return jarr; -} - -/* - * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: createEntryListenerPoller + * Method: createTopicListenerPoller * Signature: (I)I */ JNIEXPORT jint JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_createEntryListenerPoller +Java_edu_wpi_first_networktables_NetworkTablesJNI_createTopicListenerPoller (JNIEnv*, jclass, jint inst) { - return nt::CreateEntryListenerPoller(inst); + return nt::CreateTopicListenerPoller(inst); } /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: destroyEntryListenerPoller + * Method: destroyTopicListenerPoller * Signature: (I)V */ JNIEXPORT void JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyEntryListenerPoller +Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyTopicListenerPoller (JNIEnv*, jclass, jint poller) { - nt::DestroyEntryListenerPoller(poller); + nt::DestroyTopicListenerPoller(poller); } /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: addPolledEntryListener - * Signature: (ILjava/lang/String;I)I + * Method: addPolledTopicListener + * 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_addPolledTopicListener__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 arr; + std::vector arrview; + arr.reserve(len); + arrview.reserve(len); + for (size_t i = 0; i < len; ++i) { + JLocal elem{ + env, static_cast(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::AddPolledTopicListener(poller, arrview, flags); } /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: addPolledEntryListener + * Method: addPolledTopicListener * 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_addPolledTopicListener__III + (JNIEnv* env, jclass, jint poller, jint handle, jint flags) { - return nt::AddPolledEntryListener(poller, entry, flags); + return nt::AddPolledTopicListener(poller, handle, flags); } /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: pollEntryListener + * Method: readTopicListenerQueue * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object; */ JNIEXPORT jobjectArray JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_pollEntryListener +Java_edu_wpi_first_networktables_NetworkTablesJNI_readTopicListenerQueue (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::ReadTopicListenerQueue(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: removeTopicListener * Signature: (I)V */ JNIEXPORT void JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_cancelPollEntryListener +Java_edu_wpi_first_networktables_NetworkTablesJNI_removeTopicListener + (JNIEnv*, jclass, jint topicListener) +{ + nt::RemoveTopicListener(topicListener); +} + +/* + * Class: edu_wpi_first_networktables_NetworkTablesJNI + * Method: createValueListenerPoller + * Signature: (I)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_createValueListenerPoller + (JNIEnv*, jclass, jint inst) +{ + return nt::CreateValueListenerPoller(inst); +} + +/* + * Class: edu_wpi_first_networktables_NetworkTablesJNI + * Method: destroyValueListenerPoller + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_destroyValueListenerPoller (JNIEnv*, jclass, jint poller) { - nt::CancelPollEntryListener(poller); + nt::DestroyValueListenerPoller(poller); } /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: removeEntryListener + * Method: addPolledValueListener + * Signature: (III)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledValueListener + (JNIEnv* env, jclass, jint poller, jint topic, jint flags) +{ + return nt::AddPolledValueListener(poller, topic, flags); +} + +/* + * Class: edu_wpi_first_networktables_NetworkTablesJNI + * Method: readValueListenerQueue + * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object; + */ +JNIEXPORT jobjectArray JNICALL +Java_edu_wpi_first_networktables_NetworkTablesJNI_readValueListenerQueue + (JNIEnv* env, jclass, jobject inst, jint poller) +{ + return MakeJObject(env, inst, nt::ReadValueListenerQueue(poller)); +} + +/* + * Class: edu_wpi_first_networktables_NetworkTablesJNI + * Method: removeValueListener * Signature: (I)V */ JNIEXPORT void JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_removeEntryListener - (JNIEnv*, jclass, jint entryListenerUid) +Java_edu_wpi_first_networktables_NetworkTablesJNI_removeValueListener + (JNIEnv*, jclass, jint topicListener) { - 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); + nt::RemoveValueListener(topicListener); } /* @@ -1157,49 +1203,14 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledConnectionListener /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: pollConnectionListener + * Method: readConnectionListenerQueue * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object; */ JNIEXPORT jobjectArray JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_pollConnectionListener +Java_edu_wpi_first_networktables_NetworkTablesJNI_readConnectionListenerQueue (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); + return MakeJObject(env, inst, nt::ReadConnectionListenerQueue(poller)); } /* @@ -1214,214 +1225,6 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_removeConnectionListener 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, {reinterpret_cast(result.data()), result.size()}); -} - -/* - * 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, {reinterpret_cast(result.data()), result.size()}); -} - -/* - * 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; - } - auto data = val->GetRpc(); - return MakeJByteArray( - env, {reinterpret_cast(data.data()), data.size()}); -} - /* * Class: edu_wpi_first_networktables_NetworkTablesJNI * Method: setNetworkIdentity @@ -1477,12 +1280,12 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_stopLocal /* * 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"); @@ -1493,7 +1296,7 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_startServer return; } nt::StartServer(inst, JStringRef{env, persistFilename}.str(), - JStringRef{env, listenAddress}.c_str(), port); + JStringRef{env, listenAddress}.c_str(), port3, port4); } /* @@ -1510,89 +1313,26 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_stopServer /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: startClient + * Method: startClient3 * Signature: (I)V */ JNIEXPORT void JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient__I +Java_edu_wpi_first_networktables_NetworkTablesJNI_startClient3 (JNIEnv*, jclass, jint inst) { - nt::StartClient(inst); + nt::StartClient3(inst); } /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: startClient - * Signature: (ILjava/lang/String;I)V + * Method: startClient4 + * Signature: (I)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*, jclass, jint inst) { - if (!serverName) { - nullPointerEx.Throw(env, "serverName 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 names; - std::vector> servers; - names.reserve(len); - servers.reserve(len); - for (int i = 0; i < len; ++i) { - JLocal elem{ - env, static_cast(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); } /* @@ -1708,14 +1448,14 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_stopDSClient /* * 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); } /* @@ -1764,104 +1504,6 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_isConnected return nt::IsConnected(inst); } -/* - * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: savePersistent - * Signature: (ILjava/lang/String;)V - */ -JNIEXPORT void JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_savePersistent - (JNIEnv* env, jclass, jint inst, jstring filename) -{ - 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 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 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); -} - /* * Class: edu_wpi_first_networktables_NetworkTablesJNI * Method: now @@ -1963,49 +1605,14 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_addPolledLogger /* * Class: edu_wpi_first_networktables_NetworkTablesJNI - * Method: pollLogger + * Method: readLoggerQueue * Signature: (Ljava/lang/Object;I)[Ljava/lang/Object; */ JNIEXPORT jobjectArray JNICALL -Java_edu_wpi_first_networktables_NetworkTablesJNI_pollLogger +Java_edu_wpi_first_networktables_NetworkTablesJNI_readLoggerQueue (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); + return MakeJObject(env, inst, nt::ReadLoggerQueue(poller)); } /* @@ -2020,16 +1627,4 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_removeLogger 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 0000000000..54feea1cc8 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/ClientImpl.cpp @@ -0,0 +1,487 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include +#include +#include + +#include +#include +#include +#include +#include + +#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; + PubSubOptions options; + // in options as double, but copy here as integer; rounded to the nearest + // 10 ms + uint32_t periodMs; + uint64_t nextSendMs{0}; + Value lastValue; // only used for duplicate value checking + std::vector outValues; // outgoing values +}; + +class CImpl : public ServerMessageHandler { + public: + CImpl(uint64_t curTimeMs, int inst, WireConnection& wire, wpi::Logger& logger, + std::function setPeriodic); + + void ProcessIncomingBinary(wpi::span data); + void HandleLocal(std::vector&& msgs); + void SendOutgoing(wpi::span msgs); + bool SendControl(uint64_t curTimeMs); + void SendValues(uint64_t curTimeMs); + bool CheckNetworkReady(); + + // ServerMessageHandler interface + void ServerAnnounce(std::string_view name, int64_t id, + std::string_view typeStr, const wpi::json& properties, + std::optional 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 PubSubOptions& 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 m_setPeriodic; + + // indexed by publisher index + std::vector> m_publishers; + + // indexed by server-provided topic id + wpi::DenseMap 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 m_outgoing; +}; + +} // namespace + +CImpl::CImpl(uint64_t curTimeMs, int inst, WireConnection& wire, + wpi::Logger& logger, + std::function setPeriodic) + : m_inst{inst}, + m_wire{wire}, + m_logger{logger}, + 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(wpi::span 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(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; + } + 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&& msgs) { + DEBUG4("{}", "HandleLocal()"); + for (auto&& elem : msgs) { + // common case is value + if (auto msg = std::get_if(&elem.contents)) { + SetValue(msg->pubHandle, msg->value); + // setvalue puts on individual publish outgoing queues + } else if (auto msg = std::get_if(&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(&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("SendPeriodic({})", 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; + } + } +} + +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 PubSubOptions& 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(); + } + publisher->handle = pubHandle; + publisher->options = options; + publisher->periodMs = std::lround(options.periodic * 100) * 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(&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.options.keepDuplicates) { + if (value == publisher.lastValue) { + return; + } + publisher.lastValue = value; + } + 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 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 setPeriodic) + : CImpl{curTimeMs, inst, wire, logger, std::move(setPeriodic)} {} +}; + +ClientImpl::ClientImpl(uint64_t curTimeMs, int inst, WireConnection& wire, + wpi::Logger& logger, + std::function setPeriodic) + : m_impl{std::make_unique(curTimeMs, inst, wire, logger, + 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(wpi::span data) { + m_impl->ProcessIncomingBinary(data); +} + +void ClientImpl::HandleLocal(std::vector&& 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; +} + +ClientStartup::ClientStartup(ClientImpl& client) + : m_client{client}, + m_binaryWriter{client.m_impl->m_wire.SendBinary()}, + m_textWriter{client.m_impl->m_wire.SendText()} {} + +ClientStartup::~ClientStartup() { + m_client.m_impl->m_wire.Flush(); +} + +void ClientStartup::Publish(NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, + const PubSubOptions& options) { + WPI_DEBUG4(m_client.m_impl->m_logger, "StartupPublish({}, {}, {}, {})", + pubHandle, topicHandle, name, typeStr); + m_client.m_impl->Publish(pubHandle, topicHandle, name, typeStr, properties, + options); + WireEncodePublish(m_textWriter.Add(), Handle{pubHandle}.GetIndex(), name, + typeStr, properties); +} + +void ClientStartup::Subscribe(NT_Subscriber subHandle, + wpi::span prefixes, + const PubSubOptions& options) { + WPI_DEBUG4(m_client.m_impl->m_logger, "StartupSubscribe({})", subHandle); + WireEncodeSubscribe(m_textWriter.Add(), Handle{subHandle}.GetIndex(), + prefixes, options); +} + +void ClientStartup::SetValue(NT_Publisher pubHandle, const Value& value) { + WPI_DEBUG4(m_client.m_impl->m_logger, "StartupSetValue({})", pubHandle); + // Similar to Client::SetValue(), except always set lastValue and send + unsigned int index = Handle{pubHandle}.GetIndex(); + assert(index < m_client.m_impl->m_publishers.size() && + m_client.m_impl->m_publishers[index]); + auto& publisher = *m_client.m_impl->m_publishers[index]; + publisher.lastValue = value; + // only send time 0 values until we have a RTT + if (value.server_time() == 0) { + WireEncodeBinary(m_binaryWriter.Add(), index, 0, value); + } else { + publisher.outValues.emplace_back(value); + } +} 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 0000000000..ac51a2045e --- /dev/null +++ b/ntcore/src/main/native/cpp/net/ClientImpl.h @@ -0,0 +1,79 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include + +#include "NetworkInterface.h" +#include "WireConnection.h" + +namespace wpi { +class Logger; +} // namespace wpi + +namespace nt { +class PubSubOptions; +class Value; +} // namespace nt + +namespace nt::net { + +struct ClientMessage; +class ClientStartup; +class WireConnection; + +class ClientImpl { + friend class ClientStartup; + + public: + ClientImpl(uint64_t curTimeMs, int inst, WireConnection& wire, + wpi::Logger& logger, + std::function setPeriodic); + ~ClientImpl(); + + void ProcessIncomingText(std::string_view data); + void ProcessIncomingBinary(wpi::span data); + void HandleLocal(std::vector&& msgs); + + void SendControl(uint64_t curTimeMs); + void SendValues(uint64_t curTimeMs); + + void SetLocal(LocalInterface* local); + + private: + class Impl; + std::unique_ptr m_impl; +}; + +class ClientStartup final : public NetworkStartupInterface { + public: + explicit ClientStartup(ClientImpl& client); + ~ClientStartup() final; + ClientStartup(const ClientStartup&) = delete; + ClientStartup& operator=(const ClientStartup&) = delete; + + // NetworkStartupInterface interface + void Publish(NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, const PubSubOptions& options) final; + void Subscribe(NT_Subscriber subHandle, wpi::span prefixes, + const PubSubOptions& options) final; + void SetValue(NT_Publisher pubHandle, const Value& value) final; + + private: + ClientImpl& m_client; + BinaryWriter m_binaryWriter; + TextWriter m_textWriter; +}; + +} // 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 0000000000..1cec920e60 --- /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 +#include +#include +#include + +#include + +#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; + PubSubOptions 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 topicNames; + PubSubOptions 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; + Contents contents; +}; + +struct AnnounceMsg { + static constexpr std::string_view kMethodStr = "announce"; + std::string name; + int64_t id{0}; + std::string typeStr; + std::optional 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; + 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 0000000000..bbe98992c0 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/NetworkInterface.h @@ -0,0 +1,68 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include + +#include + +#include "ntcore_cpp.h" + +namespace wpi { +class json; +} // namespace wpi + +namespace nt { +class PubSubOptions; +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 NetworkStartupInterface { + public: + virtual ~NetworkStartupInterface() = default; + + virtual void Publish(NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, + const PubSubOptions& options) = 0; + virtual void Subscribe(NT_Subscriber subHandle, + wpi::span topicNames, + const PubSubOptions& options) = 0; + virtual void SetValue(NT_Publisher pubHandle, const Value& value) = 0; +}; + +class NetworkInterface : public NetworkStartupInterface { + public: + 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 Unsubscribe(NT_Subscriber subHandle) = 0; +}; + +class ILocalStorage : public LocalInterface { + public: + virtual void StartNetwork(NetworkStartupInterface& startup) = 0; + virtual void SetNetwork(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 0000000000..069accba1f --- /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 + +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 0000000000..b0b59e6aa3 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.h @@ -0,0 +1,55 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include + +#include + +#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* 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 PubSubOptions& 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, + wpi::span topicNames, + const PubSubOptions& options) final; + void Unsubscribe(NT_Subscriber subHandle) final; + void SetValue(NT_Publisher pubHandle, const Value& value) final; + + private: + wpi::mutex m_mutex; + std::vector 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 0000000000..8bdfc2e5bb --- /dev/null +++ b/ntcore/src/main/native/cpp/net/NetworkLoopQueue.inc @@ -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. + +#pragma once + +#include +#include + +#include "NetworkLoopQueue.h" +#include "ntcore_c.h" + +namespace nt::net { + +inline void NetworkLoopQueue::ReadQueue(std::vector* 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 PubSubOptions& 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, + wpi::span topicNames, + const PubSubOptions& 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 0000000000..785c403483 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/ServerImpl.cpp @@ -0,0 +1,2340 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +class VectorSet : public std::vector { + public: + using iterator = typename std::vector::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 name, std::string_view connInfo, bool local, + ServerImpl::SetPeriodicFunc setPeriodic, SImpl& server, int id, + wpi::Logger& logger) + : 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(wpi::span 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 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(); + + // returns nullptr if there is no subscriber for that topic name + SubscriberData* GetSubscriber(std::string_view name, bool special); + + std::string_view GetName() const { return m_name; } + int GetId() const { return m_id; } + + protected: + 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> m_publishers; + wpi::DenseMap> m_subscribers; + + public: + // meta topics + TopicData* m_metaPub = nullptr; + TopicData* m_metaSub = nullptr; +}; + +class ClientData4Base : public ClientData, protected ClientMessageHandler { + public: + ClientData4Base(std::string_view name, std::string_view connInfo, bool local, + ServerImpl::SetPeriodicFunc setPeriodic, SImpl& server, + int id, wpi::Logger& logger) + : ClientData{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, wpi::span topicNames, + const PubSubOptions& options) final; + void ClientUnsubscribe(int64_t subuid) final; + + void ClientSetValue(int64_t pubuid, const Value& value); + + wpi::DenseMap m_announceSent; +}; + +class ClientDataLocal final : public ClientData4Base { + friend class net::ServerStartup; + + 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(wpi::span data) final {} + + void SendValue(TopicData* topic, const Value& value, SendMode mode) final; + void SendAnnounce(TopicData* topic, std::optional 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(wpi::span msgs); +}; + +class ClientData4 final : public ClientData4Base { + public: + ClientData4(std::string_view name, std::string_view connInfo, bool local, + WireConnection& wire, ServerImpl::SetPeriodicFunc setPeriodic, + SImpl& server, int id, wpi::Logger& logger) + : ClientData4Base{name, connInfo, local, setPeriodic, server, id, logger}, + m_wire{wire} {} + + void ProcessIncomingText(std::string_view data) final; + void ProcessIncomingBinary(wpi::span data) final; + + void SendValue(TopicData* topic, const Value& value, SendMode mode) final; + void SendAnnounce(TopicData* topic, std::optional 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 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 m_outText; + std::optional 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(wpi::span data) final; + + void SendValue(TopicData* topic, const Value& value, SendMode mode) final; + void SendAnnounce(TopicData* topic, std::optional 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, + wpi::span params) final {} + void RpcResponse(unsigned int id, unsigned int uid, + wpi::span 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 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 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; + std::string typeStr; + wpi::json properties = wpi::json::object(); + bool persistent{false}; + bool retained{false}; + bool special{false}; + NT_Topic localHandle{0}; + + VectorSet publishers; + VectorSet 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, wpi::span topicNames, + int64_t subuid, const PubSubOptions& options) + : client{client}, + topicNames{topicNames.begin(), topicNames.end()}, + subuid{subuid}, + options{options}, + periodMs(std::lround(options.periodic * 100) * 10) { + if (periodMs < kMinPeriodMs) { + periodMs = kMinPeriodMs; + } + } + + void Update(wpi::span topicNames_, + const PubSubOptions& options_) { + topicNames = {topicNames_.begin(), topicNames_.end()}; + options = options_; + periodMs = std::lround(options_.periodic * 100) * 10; + if (periodMs < kMinPeriodMs) { + periodMs = kMinPeriodMs; + } + } + + bool Matches(std::string_view name, bool special); + + ClientData* client; + std::vector topicNames; + int64_t subuid; + PubSubOptions 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> m_clients; + wpi::UidVector, 16> m_topics; + wpi::StringMap 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 + 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& 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(w->context)->write(buffer, count); + }); + } + + std::vector bytes; + wpi::raw_uvector_ostream os{bytes}; + char buf[128]; +}; +} // namespace + +static void WriteOptions(mpack_writer_t& w, const PubSubOptions& options) { + int size = (options.sendAll ? 1 : 0) + (options.topicsOnly ? 1 : 0) + + (options.periodic != 0.1 ? 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.periodic != 0.1) { + mpack_write_str(&w, "periodic"); + mpack_write_float(&w, options.periodic); + } + 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))); + } +} + +SubscriberData* ClientData::GetSubscriber(std::string_view name, bool special) { + for (auto&& subPair : m_subscribers) { + SubscriberData* subscriber = subPair.getSecond().get(); + if (subscriber->Matches(name, special)) { + return subscriber; + } + } + return nullptr; +} + +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(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, + wpi::span topicNames, + const PubSubOptions& 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(this, topicNames, subuid, options); + } + + // limit subscriber min period + if (sub->periodMs < kMinPeriodMs) { + sub->periodMs = kMinPeriodMs; + } + + // see if this immediately subscribes to any topics + bool updatedPeriodic = false; + for (auto&& topic : m_server.m_topics) { + bool removed = false; + if (replace) { + removed = topic->subscribers.Remove(sub.get()); + } + + 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 (added || removed) { + // update periodic sender (if not local) + if (!m_local) { + m_periodMs = std::gcd(m_periodMs, sub->periodMs); + updatedPeriodic = true; + } + } + + if (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); + } + } + } + if (updatedPeriodic) { + if (m_periodMs < kMinPeriodMs) { + m_periodMs = kMinPeriodMs; + } + m_setPeriodic(m_periodMs); + } + + // 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 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(wpi::span 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(&elem.contents)) { + ClientSetValue(msg->pubHandle, msg->value); + } else if (auto msg = std::get_if(&elem.contents)) { + ClientPublish(msg->pubHandle, msg->name, msg->typeStr, msg->properties); + } else if (auto msg = std::get_if(&elem.contents)) { + ClientUnpublish(msg->pubHandle); + } else if (auto msg = std::get_if(&elem.contents)) { + ClientSetProperties(msg->name, msg->update); + } else if (auto msg = std::get_if(&elem.contents)) { + ClientSubscribe(msg->subHandle, msg->topicNames, msg->options); + } else if (auto msg = std::get_if(&elem.contents)) { + ClientUnsubscribe(msg->subHandle); + } + } +} + +void ClientData4::ProcessIncomingText(std::string_view data) { + WireDecodeText(data, *this, m_logger); +} + +void ClientData4::ProcessIncomingBinary(wpi::span 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(&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 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(&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(wpi::span 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 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; + } + m_name = self_id; + // create a unique name if none provided + if (m_name.empty()) { + 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( + this, wpi::span{{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(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(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(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(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()) { + persistent = *val; + } + } + + auto retainedIt = properties.find("retained"); + if (retainedIt != properties.end()) { + if (auto val = retainedIt->get_ptr()) { + 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(*this, 0, logger)); + m_localClient = static_cast(m_clients.back().get()); +} + +int SImpl::AddClient(std::string_view name, std::string_view connInfo, + bool local, WireConnection& wire, + ServerImpl::SetPeriodicFunc setPeriodic) { + size_t index = m_clients.size(); + // find an empty slot and ensure there's no duplicates + // just do a linear search as number of clients is typically small (<10) + for (size_t i = 0, end = index; i < end; ++i) { + auto& clientData = m_clients[i]; + if (clientData && clientData->GetName() == name) { + return -1; // don't allow duplicate client names + } else if (!clientData && index == end) { + index = i; + } + } + if (index == m_clients.size()) { + m_clients.emplace_back(); + } + + auto& clientData = m_clients[index]; + clientData = std::make_unique(name, connInfo, local, wire, + std::move(setPeriodic), *this, + index, m_logger); + + // create client meta topics + clientData->m_metaPub = CreateMetaTopic(fmt::format("$clientpub${}", name)); + clientData->m_metaSub = CreateMetaTopic(fmt::format("$clientsub${}", name)); + + // update meta topics + clientData->UpdateMetaClientPub(); + clientData->UpdateMetaClientSub(); + + wire.Flush(); + + DEBUG3("AddClient('{}', '{}') -> {}", name, connInfo, index); + return 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 == end) { + index = i; + } + } + if (index == m_clients.size()) { + m_clients.emplace_back(); + } + + m_clients[index] = std::make_unique( + 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 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(); + 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(); + 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()) { + 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()) { + 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()) { + value = Value::MakeInteger(*v, time); + } else if (auto v = valueIt->second.get_ptr()) { + 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()) { + 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()) { + 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()) { + value = Value::MakeString(*v, time); + } else { + error = "value type mismatch, expected string"; + goto err; + } + } else if (*typeStr == "boolean[]") { + auto arr = valueIt->second.get_ptr(); + if (!arr) { + error = "value type mismatch, expected array"; + goto err; + } + std::vector elems; + for (auto&& jelem : valueIt->second) { + if (auto v = jelem.get_ptr()) { + 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(); + if (!arr) { + error = "value type mismatch, expected array"; + goto err; + } + std::vector elems; + for (auto&& jelem : valueIt->second) { + if (auto v = jelem.get_ptr()) { + elems.push_back(*v); + } else if (auto v = jelem.get_ptr()) { + 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(); + if (!arr) { + error = "value type mismatch, expected array"; + goto err; + } + std::vector elems; + for (auto&& jelem : valueIt->second) { + if (auto v = jelem.get_ptr()) { + 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(); + if (!arr) { + error = "value type mismatch, expected array"; + goto err; + } + std::vector elems; + for (auto&& jelem : valueIt->second) { + if (auto v = jelem.get_ptr()) { + 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(); + if (!arr) { + error = "value type mismatch, expected array"; + goto err; + } + std::vector elems; + for (auto&& jelem : valueIt->second) { + if (auto v = jelem.get_ptr()) { + 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::vector 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(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 + bool hasSubscriber = false; + if (auto subscriber = aClient->GetSubscriber(name, topic->special)) { + topic->subscribers.Add(subscriber); + hasSubscriber = true; + } + + // don't announce to this client if no subscribers + if (!hasSubscriber) { + 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 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 timestamp newer + if (!topic->lastValue || value.time() > topic->lastValue.time()) { + DEBUG4("updating '{}' last value (time was {} is {})", topic->name, + topic->lastValue.time(), value.time()); + topic->lastValue = value; + + // 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 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& 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 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(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(wpi::span msgs) { + // just map as a normal client into client=0 calls + m_impl->m_localClient->HandleLocal(msgs); +} + +void ServerImpl::SetLocal(LocalInterface* local) { + 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, + wpi::span data) { + m_impl->m_clients[clientId]->ProcessIncomingBinary(data); +} + +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& 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); +} + +void ServerStartup::Publish(NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, + const PubSubOptions& options) { + m_server.m_impl->m_localClient->ClientPublish(pubHandle, name, typeStr, + properties); +} + +void ServerStartup::Subscribe(NT_Subscriber subHandle, + wpi::span topicNames, + const PubSubOptions& options) { + m_server.m_impl->m_localClient->ClientSubscribe(subHandle, topicNames, + options); +} + +void ServerStartup::SetValue(NT_Publisher pubHandle, const Value& value) { + m_server.m_impl->m_localClient->ClientSetValue(pubHandle, value); +} 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 0000000000..18669c1d6a --- /dev/null +++ b/ntcore/src/main/native/cpp/net/ServerImpl.h @@ -0,0 +1,94 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include + +#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 ServerStartup; +class WireConnection; + +class ServerImpl final { + friend class ServerStartup; + + public: + using SetPeriodicFunc = std::function; + using Connected3Func = + std::function; + + explicit ServerImpl(wpi::Logger& logger); + ~ServerImpl(); + + void SendControl(uint64_t curTimeMs); + void SendValues(int clientId, uint64_t curTimeMs); + + void HandleLocal(wpi::span msgs); + void SetLocal(LocalInterface* local); + + void ProcessIncomingText(int clientId, std::string_view data); + void ProcessIncomingBinary(int clientId, wpi::span data); + + // Returns -1 if cannot add client (e.g. due to duplicate name). + // Caller must ensure WireConnection lifetime lasts until RemoveClient() call. + 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& 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 m_impl; +}; + +class ServerStartup final : public NetworkStartupInterface { + public: + explicit ServerStartup(ServerImpl& server) : m_server{server} {} + + // NetworkStartupInterface interface + void Publish(NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, const PubSubOptions& options) final; + void Subscribe(NT_Subscriber subHandle, + wpi::span topicNames, + const PubSubOptions& options) final; + void SetValue(NT_Publisher pubHandle, const Value& value) final; + + private: + ServerImpl& m_server; +}; + +} // 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 0000000000..921b8cd829 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/WebSocketConnection.cpp @@ -0,0 +1,122 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "WebSocketConnection.h" + +#include +#include + +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, + wpi::span{frame.bufs->begin() + frame.start, + frame.bufs->begin() + frame.end}); + } + + ++m_sendsActive; + m_ws.SendFrames(m_ws_frames, [this](auto bufs, auto) { + m_buf_pool.insert(m_buf_pool.end(), bufs.begin(), bufs.end()); + if (m_sendsActive > 0) { + --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 0000000000..0f0fb92307 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/WebSocketConnection.h @@ -0,0 +1,66 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include +#include +#include + +#include "WireConnection.h" + +namespace nt::net { + +class WebSocketConnection final : public WireConnection { + 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* bufs, + size_t start, size_t end) + : opcode{opcode}, bufs{bufs}, start{start}, end{end} {} + uint8_t opcode; + wpi::SmallVectorImpl* bufs; + size_t start; + size_t end; + }; + std::vector m_frames; + std::vector m_ws_frames; // to reduce allocs + wpi::SmallVector m_text_buffers; + wpi::SmallVector m_binary_buffers; + std::vector 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 0000000000..2a79a12adf --- /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 + +#include + +#include + +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 0000000000..9321adf49d --- /dev/null +++ b/ntcore/src/main/native/cpp/net/WireDecoder.cpp @@ -0,0 +1,564 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include +#include +#include + +#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()) { + *num = *v; + } else if (auto v = val.get_ptr()) { + *num = *v; + } else if (auto v = val.get_ptr()) { + *num = *v; + } else { + return false; + } + return true; +} + +static bool GetNumber(wpi::json& val, int64_t* num) { + if (auto v = val.get_ptr()) { + *num = *v; + } else if (auto v = val.get_ptr()) { + *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(); + 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* 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(); + 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(); + 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 +static void WireDecodeTextImpl(std::string_view in, T& out, + wpi::Logger& logger) { + static_assert(std::is_same_v || + std::is_same_v, + "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(); + 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(); + if (!params) { + error = "params must be an object"; + goto err; + } + + if constexpr (std::is_same_v) { + 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 + PubSubOptions options; + auto optionsIt = params->find("options"); + if (optionsIt != params->end()) { + auto joptions = optionsIt->second.get_ptr(); + if (!joptions) { + error = "options must be an object"; + goto err; + } + + // periodic + auto periodicIt = joptions->find("periodic"); + if (periodicIt != joptions->end()) { + if (!GetNumber(periodicIt->second, &options.periodic)) { + error = "periodic value must be a number"; + goto err; + } + } + + // send all changes + auto sendAllIt = joptions->find("all"); + if (sendAllIt != joptions->end()) { + auto sendAll = sendAllIt->second.get_ptr(); + 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(); + 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(); + if (!prefixMatch) { + error = "prefix value must be a boolean"; + goto err; + } + options.prefixMatch = *prefixMatch; + } + } + + // topic names + std::vector 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) { + 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 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(); + 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(wpi::span* in, int64_t* outId, + Value* outValue, std::string* error, + int64_t localTimeOffset) { + mpack_reader_t reader; + mpack_reader_init_data(&reader, reinterpret_cast(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(data), length}, 1); + } + mpack_done_bin(&reader); + break; + } + case 16: { // boolean array + auto length = mpack_expect_array(&reader); + std::vector 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 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 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 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 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 0000000000..c1cbddaac2 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/WireDecoder.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 + +#include +#include +#include + +#include + +namespace wpi { +class Logger; +class json; +} // namespace wpi + +namespace nt { +class PubSubOptions; +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, + wpi::span topicNames, + const PubSubOptions& 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 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(wpi::span* 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 0000000000..d0edc2f1ae --- /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 + +#include +#include +#include + +#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 +static void EncodePrefixes(wpi::raw_ostream& os, wpi::span 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 +static void WireEncodeSubscribeImpl(wpi::raw_ostream& os, int64_t subuid, + wpi::span topicNames, + const PubSubOptions& 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.periodic != 0.1) { + if (!first) { + os << ','; + } + os << "\"periodic\":"; + s.dump_float(options.periodic); + } + os << "},\"topics\":"; + EncodePrefixes(os, topicNames, s); + os << ",\"subuid\":"; + s.dump_integer(subuid); + os << "}}"; +} + +void nt::net::WireEncodeSubscribe(wpi::raw_ostream& os, int64_t subuid, + wpi::span topicNames, + const PubSubOptions& options) { + WireEncodeSubscribeImpl(os, subuid, topicNames, options); +} + +void nt::net::WireEncodeSubscribe(wpi::raw_ostream& os, int64_t subuid, + wpi::span topicNames, + const PubSubOptions& 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(&msg.contents)) { + WireEncodePublish(os, Handle{m->pubHandle}.GetIndex(), m->name, m->typeStr, + m->properties); + } else if (auto m = std::get_if(&msg.contents)) { + WireEncodeUnpublish(os, Handle{m->pubHandle}.GetIndex()); + } else if (auto m = std::get_if(&msg.contents)) { + WireEncodeSetProperties(os, m->name, m->update); + } else if (auto m = std::get_if(&msg.contents)) { + WireEncodeSubscribe(os, Handle{m->subHandle}.GetIndex(), m->topicNames, + m->options); + } else if (auto m = std::get_if(&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 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(&msg.contents)) { + WireEncodeAnnounce(os, m->name, m->id, m->typeStr, m->properties, + m->pubuid); + } else if (auto m = std::get_if(&msg.contents)) { + WireEncodeUnannounce(os, m->name, m->id); + } else if (auto m = std::get_if(&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(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(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 0000000000..6ea4fea9d2 --- /dev/null +++ b/ntcore/src/main/native/cpp/net/WireEncoder.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 +#include +#include + +#include + +namespace wpi { +class json; +class raw_ostream; +} // namespace wpi + +namespace nt { +class PubSubOptions; +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, + wpi::span topicNames, + const PubSubOptions& options); +void WireEncodeSubscribe(wpi::raw_ostream& os, int64_t subuid, + wpi::span topicNames, + const PubSubOptions& 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 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 0000000000..13f6aee83b --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/ClientImpl3.cpp @@ -0,0 +1,672 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include +#include +#include + +#include +#include +#include +#include + +#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; + PubSubOptions 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 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 publishers; +}; + +class CImpl : public MessageHandler3 { + public: + CImpl(uint64_t curTimeMs, int inst, WireConnection3& wire, + wpi::Logger& logger, + std::function setPeriodic); + + void ProcessIncoming(wpi::span data); + void HandleLocal(wpi::span 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 PubSubOptions& 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, + wpi::span params) final {} + void RpcResponse(unsigned int id, unsigned int uid, + wpi::span 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 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> m_publishers; + + State m_state{kStateInitial}; + WireDecoder3 m_decoder; + std::string m_remoteId; + std::function m_handshakeSucceeded; + + std::vector> m_outgoingFlags; + + using NameMap = wpi::StringMap>; + using IdMap = std::vector; + + NameMap m_nameMap; + IdMap m_idMap; + + Entry* GetOrNewEntry(std::string_view name) { + auto& entry = m_nameMap[name]; + if (!entry) { + entry = std::make_unique(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 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(wpi::span data) { + DEBUG4("received {} bytes", data.size()); + if (!m_decoder.Execute(&data)) { + m_wire.Disconnect(m_decoder.GetError()); + } +} + +void CImpl::HandleLocal(wpi::span msgs) { + for (const auto& elem : msgs) { // NOLINT + // common case is value + if (auto msg = std::get_if(&elem.contents)) { + SetValue(msg->pubHandle, msg->value); + } else if (auto msg = std::get_if(&elem.contents)) { + Publish(msg->pubHandle, msg->topicHandle, msg->name, msg->typeStr, + msg->properties, msg->options); + } else if (auto msg = std::get_if(&elem.contents)) { + Unpublish(msg->pubHandle, msg->topicHandle); + } else if (auto msg = std::get_if(&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 PubSubOptions& 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(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.periodic * 100) * 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()) { + 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 setPeriodic) + : CImpl{curTimeMs, inst, wire, logger, std::move(setPeriodic)} {} +}; + +ClientImpl3::ClientImpl3(uint64_t curTimeMs, int inst, WireConnection3& wire, + wpi::Logger& logger, + std::function setPeriodic) + : m_impl{std::make_unique(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 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(wpi::span data) { + m_impl->ProcessIncoming(data); +} + +void ClientImpl3::HandleLocal(wpi::span 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; +} + +ClientStartup3::ClientStartup3(ClientImpl3& client) : m_client{client} {} + +ClientStartup3::~ClientStartup3() = default; + +void ClientStartup3::Publish(NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, + const PubSubOptions& options) { + WPI_DEBUG4(m_client.m_impl->m_logger, "StartupPublish({}, {}, {}, {})", + pubHandle, topicHandle, name, typeStr); + m_client.m_impl->Publish(pubHandle, topicHandle, name, typeStr, properties, + options); +} + +void ClientStartup3::Subscribe(NT_Subscriber subHandle, + wpi::span prefixes, + const PubSubOptions& options) { + // NT3 ignores subscribes, so no action required +} + +void ClientStartup3::SetValue(NT_Publisher pubHandle, const Value& value) { + WPI_DEBUG4(m_client.m_impl->m_logger, "StartupSetValue({})", pubHandle); + // Similar to Client::SetValue(), except always set value and queue + unsigned int index = Handle{pubHandle}.GetIndex(); + assert(index < m_client.m_impl->m_publishers.size() && + m_client.m_impl->m_publishers[index]); + auto& publisher = *m_client.m_impl->m_publishers[index]; + publisher.entry->value = value; + publisher.outValues.emplace_back(value); +} 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 0000000000..6d6bab2e83 --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/ClientImpl3.h @@ -0,0 +1,73 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +#include +#include +#include +#include + +#include + +#include "net/NetworkInterface.h" + +namespace wpi { +class Logger; +} // namespace wpi + +namespace nt::net { +struct ClientMessage; +class LocalInterface; +} // namespace nt::net + +namespace nt::net3 { + +class ClientStartup3; +class WireConnection3; + +class ClientImpl3 { + friend class ClientStartup3; + + public: + explicit ClientImpl3(uint64_t curTimeMs, int inst, WireConnection3& wire, + wpi::Logger& logger, + std::function setPeriodic); + ~ClientImpl3(); + + void Start(std::string_view selfId, std::function succeeded); + void ProcessIncoming(wpi::span data); + void HandleLocal(wpi::span msgs); + + void SendPeriodic(uint64_t curTimeMs); + + void SetLocal(net::LocalInterface* local); + + private: + class Impl; + std::unique_ptr m_impl; +}; + +class ClientStartup3 final : public net::NetworkStartupInterface { + public: + explicit ClientStartup3(ClientImpl3& client); + ~ClientStartup3() final; + ClientStartup3(const ClientStartup3&) = delete; + ClientStartup3& operator=(const ClientStartup3&) = delete; + + // NetworkStartupInterface interface + void Publish(NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, const PubSubOptions& options) final; + void Subscribe(NT_Subscriber subHandle, wpi::span prefixes, + const PubSubOptions& options) final; + void SetValue(NT_Publisher pubHandle, const Value& value) final; + + private: + ClientImpl3& m_client; +}; + +} // 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 0000000000..433219680d --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/Message3.h @@ -0,0 +1,155 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include + +#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; } + wpi::span bytes() const { + return {reinterpret_cast(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, + wpi::span params) { + Message3 msg{kExecuteRpc, {}}; + msg.m_str.assign(reinterpret_cast(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, + wpi::span result) { + Message3 msg{kRpcResponse, {}}; + msg.m_str.assign(reinterpret_cast(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/SequenceNumber.cpp b/ntcore/src/main/native/cpp/net3/SequenceNumber.cpp similarity index 94% rename from ntcore/src/main/native/cpp/SequenceNumber.cpp rename to ntcore/src/main/native/cpp/net3/SequenceNumber.cpp index e25e636a4a..04bec2dc33 100644 --- a/ntcore/src/main/native/cpp/SequenceNumber.cpp +++ b/ntcore/src/main/native/cpp/net3/SequenceNumber.cpp @@ -4,7 +4,7 @@ #include "SequenceNumber.h" -namespace nt { +namespace nt::net3 { bool operator<(const SequenceNumber& lhs, const SequenceNumber& rhs) { if (lhs.m_value < rhs.m_value) { @@ -26,4 +26,4 @@ bool operator>(const SequenceNumber& lhs, const SequenceNumber& rhs) { } } -} // namespace nt +} // namespace nt::net3 diff --git a/ntcore/src/main/native/cpp/SequenceNumber.h b/ntcore/src/main/native/cpp/net3/SequenceNumber.h similarity index 92% rename from ntcore/src/main/native/cpp/SequenceNumber.h rename to ntcore/src/main/native/cpp/net3/SequenceNumber.h index 719d85c9f9..e5ac2484c4 100644 --- a/ntcore/src/main/native/cpp/SequenceNumber.h +++ b/ntcore/src/main/native/cpp/net3/SequenceNumber.h @@ -2,10 +2,9 @@ // Open Source 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_ +#pragma once -namespace nt { +namespace nt::net3 { /* A sequence number per RFC 1982 */ class SequenceNumber { @@ -57,6 +56,4 @@ inline bool operator!=(const SequenceNumber& lhs, const SequenceNumber& rhs) { return lhs.m_value != rhs.m_value; } -} // namespace nt - -#endif // NTCORE_SEQUENCENUMBER_H_ +} // 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 0000000000..b3f48b29e9 --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/UvStreamConnection3.cpp @@ -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. + +#include "UvStreamConnection3.h" + +#include + +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, [this](auto bufs, auto) { + m_buf_pool.insert(m_buf_pool.end(), bufs.begin(), bufs.end()); + if (m_sendsActive > 0) { + --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 0000000000..ae5bb9c5c4 --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/UvStreamConnection3.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 +#include +#include + +#include +#include +#include + +#include "net3/WireConnection3.h" + +namespace wpi::uv { +class Stream; +} // namespace wpi::uv + +namespace nt::net3 { + +class UvStreamConnection3 final : public WireConnection3 { + 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 m_buffers; + std::vector 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 0000000000..85453d77b6 --- /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 + +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 0000000000..9e14724c0a --- /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 +#include +#include +#include + +#include +#include +#include +#include + +#include "Message3.h" + +using namespace nt; +using namespace nt::net3; + +namespace { + +class SimpleValueReader { + public: + std::optional Read16(wpi::span* in); + std::optional Read32(wpi::span* in); + std::optional Read64(wpi::span* in); + std::optional ReadDouble(wpi::span* in); + + private: + uint64_t m_value = 0; + int m_count = 0; +}; + +struct StringReader { + void SetLen(uint64_t len_) { + len = len_; + buf.clear(); + } + + std::optional len; + std::string buf; +}; + +struct RawReader { + void SetLen(uint64_t len_) { + len = len_; + buf.clear(); + } + + std::optional len; + std::vector 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 ints; + std::vector doubles; + std::vector 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(wpi::span* in); + + std::nullopt_t EmitError(std::string_view msg) { + m_state = kError; + m_error = msg; + return std::nullopt; + } + + std::optional ReadString(wpi::span* in); + std::optional> ReadRaw(wpi::span* in); + std::optional ReadType(wpi::span* in); + std::optional ReadValue(wpi::span* in); +}; + +} // namespace + +static uint8_t Read8(wpi::span* in) { + uint8_t val = in->front(); + *in = wpi::drop_front(*in); + return val; +} + +std::optional SimpleValueReader::Read16( + wpi::span* 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(m_value); + m_count = 0; + m_value = 0; + return val; + } + } + return std::nullopt; +} + +std::optional SimpleValueReader::Read32( + wpi::span* 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(m_value); + m_count = 0; + m_value = 0; + return val; + } + } + return std::nullopt; +} + +std::optional SimpleValueReader::Read64( + wpi::span* 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 SimpleValueReader::ReadDouble( + wpi::span* in) { + if (auto val = Read64(in)) { + return wpi::BitsToDouble(val.value()); + } else { + return std::nullopt; + } +} + +void WDImpl::Execute(wpi::span* 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(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 WDImpl::ReadString(wpi::span* 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(m_stringReader.len.value() - + m_stringReader.buf.size())); // NOLINT + m_stringReader.buf.append(reinterpret_cast(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> WDImpl::ReadRaw( + wpi::span* 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(in->size()), + static_cast(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 WDImpl::ReadType(wpi::span* 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 WDImpl::ReadValue(wpi::span* 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(wpi::span* 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 0000000000..f062eee59e --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/WireDecoder3.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 + +#include +#include + +#include + +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, + wpi::span params) = 0; + virtual void RpcResponse(unsigned int id, unsigned int uid, + wpi::span 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(wpi::span* in); + + void SetError(std::string_view message); + std::string GetError() const; + + private: + struct Impl; + std::unique_ptr 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 0000000000..41bd7ddc68 --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/WireEncoder3.cpp @@ -0,0 +1,327 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include +#include +#include +#include + +#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 << wpi::span{{static_cast((val >> 8) & 0xff), + static_cast(val & 0xff)}}; +} + +static void Write32(wpi::raw_ostream& os, uint32_t val) { + os << wpi::span{{static_cast((val >> 24) & 0xff), + static_cast((val >> 16) & 0xff), + static_cast((val >> 8) & 0xff), + static_cast(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 << wpi::span{{static_cast((v >> 56) & 0xff), + static_cast((v >> 48) & 0xff), + static_cast((v >> 40) & 0xff), + static_cast((v >> 32) & 0xff), + static_cast((v >> 24) & 0xff), + static_cast((v >> 16) & 0xff), + static_cast((v >> 8) & 0xff), + static_cast(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, wpi::span 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, + wpi::span 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, + wpi::span 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 0000000000..4163d6b83b --- /dev/null +++ b/ntcore/src/main/native/cpp/net3/WireEncoder3.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 + +#include + +#include + +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, wpi::span params); +void WireEncodeRpcResponse(wpi::raw_ostream& os, unsigned int id, + unsigned int uid, wpi::span 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 0c28dd4b6f..f5d9bc39b2 100644 --- a/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp +++ b/ntcore/src/main/native/cpp/networktables/NetworkTable.cpp @@ -12,7 +12,18 @@ #include #include +#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" using namespace nt; @@ -78,11 +89,7 @@ NetworkTable::NetworkTable(NT_Inst inst, std::string_view path, const private_init&) : m_inst(inst), m_path(path) {} -NetworkTable::~NetworkTable() { - for (auto i : m_listeners) { - RemoveEntryListener(i); - } -} +NetworkTable::~NetworkTable() {} NetworkTableInstance NetworkTable::GetInstance() const { return NetworkTableInstance{m_inst}; @@ -99,78 +106,58 @@ NetworkTableEntry NetworkTable::GetEntry(std::string_view key) const { 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(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(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>(); - - 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::GetSubTable( @@ -183,25 +170,52 @@ bool NetworkTable::ContainsKey(std::string_view key) const { 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 NetworkTable::GetTopicInfo(int types) const { + std::vector 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 NetworkTable::GetTopics(int types) const { + std::vector 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 NetworkTable::GetKeys(int types) const { std::vector 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 +223,9 @@ std::vector NetworkTable::GetKeys(int types) const { std::vector NetworkTable::GetSubTables() const { std::vector 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 +247,6 @@ bool NetworkTable::IsPersistent(std::string_view key) const { 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); } @@ -332,44 +331,34 @@ std::vector NetworkTable::GetStringArray( return GetEntry(key).GetStringArray(defaultValue); } -bool NetworkTable::PutRaw(std::string_view key, std::string_view value) { +bool NetworkTable::PutRaw(std::string_view key, + wpi::span value) { return GetEntry(key).SetRaw(value); } bool NetworkTable::SetDefaultRaw(std::string_view key, - std::string_view defaultValue) { + wpi::span defaultValue) { return GetEntry(key).SetDefaultRaw(defaultValue); } -std::string NetworkTable::GetRaw(std::string_view key, - std::string_view defaultValue) const { +std::vector NetworkTable::GetRaw( + std::string_view key, wpi::span defaultValue) const { return GetEntry(key).GetRaw(defaultValue); } -bool NetworkTable::PutValue(std::string_view key, - std::shared_ptr 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 defaultValue) { + const Value& defaultValue) { return GetEntry(key).SetDefaultValue(defaultValue); } -std::shared_ptr NetworkTable::GetValue(std::string_view key) const { +Value NetworkTable::GetValue(std::string_view key) const { return GetEntry(key).GetValue(); } std::string_view NetworkTable::GetPath() const { return m_path; } - -const char* NetworkTable::SaveEntries(std::string_view filename) const { - return nt::SaveEntries(m_inst, filename, fmt::format("{}/", m_path)); -} - -const char* NetworkTable::LoadEntries( - std::string_view filename, - std::function warn) { - return nt::LoadEntries(m_inst, filename, fmt::format("{}/", m_path), warn); -} diff --git a/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp b/ntcore/src/main/native/cpp/networktables/NetworkTableEntry.cpp index abe64e5e81..12bb5d8936 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 4566b30529..07963a9b4e 100644 --- a/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp +++ b/ntcore/src/main/native/cpp/networktables/NetworkTableInstance.cpp @@ -7,8 +7,75 @@ #include #include +#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/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 NetworkTableInstance::GetTable( std::string_view key) const { if (key.empty() || key == "/") { @@ -23,29 +90,14 @@ std::shared_ptr NetworkTableInstance::GetTable( } } -void NetworkTableInstance::StartClient( - wpi::span servers, unsigned int port) { - wpi::SmallVector, 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 servers, unsigned int port) { - wpi::SmallVector, 8> server_ports; + std::vector> 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); -} - -NT_EntryListener NetworkTableInstance::AddEntryListener( - std::string_view prefix, - std::function callback, - unsigned int flags) const { - return ::nt::AddEntryListener(m_handle, prefix, callback, flags); + SetServer(serversArr); } NT_ConnectionListener NetworkTableInstance::AddConnectionListener( 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 0000000000..8d73e06230 --- /dev/null +++ b/ntcore/src/main/native/cpp/networktables/Topic.cpp @@ -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. + +#include "networktables/Topic.h" + +#include + +#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( + wpi::span options) { + return GenericSubscribe("", options); +} + +GenericSubscriber Topic::GenericSubscribe( + std::string_view typeString, wpi::span options) { + return GenericSubscriber{::nt::Subscribe( + m_handle, ::nt::GetTypeFromString(typeString), typeString, options)}; +} + +GenericPublisher Topic::GenericPublish(std::string_view typeString, + wpi::span options) { + return GenericPublisher{::nt::Publish( + m_handle, ::nt::GetTypeFromString(typeString), typeString, options)}; +} + +GenericPublisher Topic::GenericPublishEx( + std::string_view typeString, const wpi::json& properties, + wpi::span options) { + return GenericPublisher{::nt::PublishEx(m_handle, + ::nt::GetTypeFromString(typeString), + typeString, properties, options)}; +} + +GenericEntry Topic::GetGenericEntry(wpi::span options) { + return GetGenericEntry("", options); +} + +GenericEntry Topic::GetGenericEntry(std::string_view typeString, + wpi::span 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 d910892976..0b3ad7b6d9 100644 --- a/ntcore/src/main/native/cpp/ntcore_c.cpp +++ b/ntcore/src/main/native/cpp/ntcore_c.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include "Value_internal.h" @@ -19,18 +21,12 @@ using namespace nt; // Conversion helpers -static void ConvertToC(std::string_view in, char** out) { - *out = static_cast(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,48 +37,17 @@ static void ConvertToC(const ConnectionInfo& in, NT_ConnectionInfo* out) { 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( - 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( - 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) { +static void ConvertToC(const TopicNotification& in, NT_TopicNotification* out) { out->listener = in.listener; - out->entry = in.entry; - ConvertToC(in.name, &out->name); - ConvertToC(*in.value, &out->value); + ConvertToC(in.info, &out->info); + out->flags = in.flags; +} + +static void ConvertToC(const ValueNotification& in, NT_ValueNotification* out) { + out->listener = in.listener; + out->topic = in.topic; + out->subentry = in.subentry; + ConvertToC(in.value, &out->value); out->flags = in.flags; } @@ -101,33 +66,22 @@ static void ConvertToC(const LogMessage& in, NT_LogMessage* out) { ConvertToC(in.message, &out->message); } -template -static O* ConvertToC(const std::vector& in, size_t* out_len) { - if (!out_len) { - return nullptr; - } - *out_len = in.size(); - if (in.empty()) { - return nullptr; - } - O* out = static_cast(wpi::safe_malloc(sizeof(O) * in.size())); - for (size_t i = 0; i < in.size(); ++i) { - ConvertToC(in[i], &out[i]); - } - return out; -} - static void DisposeConnectionInfo(NT_ConnectionInfo* info) { std::free(info->remote_id.str); 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); +static void DisposeTopicNotification(NT_TopicNotification* info) { + DisposeTopicInfo(&info->info); +} + +static void DisposeValueNotification(NT_ValueNotification* info) { NT_DisposeValue(&info->value); } @@ -135,38 +89,6 @@ 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])); - } - - out.results.reserve(in.num_results); - for (size_t i = 0; i < in.num_results; ++i) { - out.results.push_back(ConvertFromC(in.results[i])); - } - - return out; -} - extern "C" { /* @@ -197,21 +119,6 @@ NT_Entry NT_GetEntry(NT_Inst inst, const char* name, size_t name_len) { 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( - 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 +140,7 @@ void NT_GetEntryValue(NT_Entry entry, struct NT_Value* value) { if (!v) { return; } - ConvertToC(*v, value); + ConvertToC(v, value); } int NT_SetDefaultEntryValue(NT_Entry entry, @@ -245,10 +152,6 @@ int NT_SetEntryValue(NT_Entry entry, const struct NT_Value* value) { 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 +160,50 @@ unsigned int NT_GetEntryFlags(NT_Entry entry) { 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::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(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(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 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(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(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 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(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,85 +211,295 @@ NT_Bool NT_GetEntryInfoHandle(NT_Entry entry, struct NT_EntryInfo* info) { 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; + } + nt::SetTopicProperties(topic, j); + return true; +} + +NT_Subscriber NT_Subscribe(NT_Topic topic, NT_Type type, const char* typeStr, + const struct NT_PubSubOption* options, + size_t options_len) { + wpi::SmallVector o; + o.reserve(options_len); + for (size_t i = 0; i < options_len; ++i) { + o.emplace_back(options[i].type, options[i].value); + } + return nt::Subscribe(topic, type, typeStr, o); +} + +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_PubSubOption* options, + size_t options_len) { + wpi::SmallVector o; + o.reserve(options_len); + for (size_t i = 0; i < options_len; ++i) { + o.emplace_back(options[i].type, options[i].value); + } + return nt::Publish(topic, type, typeStr, o); +} + +NT_Publisher NT_PublishEx(NT_Topic topic, NT_Type type, const char* typeStr, + const char* properties, + const struct NT_PubSubOption* options, + size_t options_len) { + 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 {}; + } + } + + wpi::SmallVector o; + o.reserve(options_len); + for (size_t i = 0; i < options_len; ++i) { + o.emplace_back(options[i].type, options[i].value); + } + + return nt::PublishEx(topic, type, typeStr, j, o); +} + +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_PubSubOption* options, + size_t options_len) { + wpi::SmallVector o; + o.reserve(options_len); + for (size_t i = 0; i < options_len; ++i) { + o.emplace_back(options[i].type, options[i].value); + } + return nt::GetEntry(topic, type, typeStr, o); +} + +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_TopicListener NT_AddTopicListener(NT_Inst inst, const char* prefix, + size_t prefix_len, unsigned int mask, + void* data, + NT_TopicListenerCallback callback) { + std::string_view p{prefix, prefix_len}; + return nt::AddTopicListener(inst, {{p}}, mask, [=](auto& event) { + NT_TopicNotification event_c; + ConvertToC(event, &event_c); + callback(data, &event_c); + DisposeTopicNotification(&event_c); + }); } -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); +NT_TopicListener NT_AddTopicListenerMultiple( + NT_Inst inst, const NT_String* prefixes, size_t prefixes_len, + unsigned int mask, void* data, NT_TopicListenerCallback callback) { + wpi::SmallVector p; + p.reserve(prefixes_len); + for (size_t i = 0; i < prefixes_len; ++i) { + p.emplace_back(prefixes[i].str, prefixes[i].len); + } + return nt::AddTopicListener(inst, p, mask, [=](auto& event) { + NT_TopicNotification event_c; + ConvertToC(event, &event_c); + callback(data, &event_c); + DisposeTopicNotification(&event_c); + }); } -NT_EntryListenerPoller NT_CreateEntryListenerPoller(NT_Inst inst) { - return nt::CreateEntryListenerPoller(inst); +NT_TopicListener NT_AddTopicListenerSingle(NT_Topic topic, unsigned int mask, + void* data, + NT_TopicListenerCallback callback) { + return nt::AddTopicListener(topic, mask, [=](auto& event) { + NT_TopicNotification event_c; + ConvertToC(event, &event_c); + callback(data, &event_c); + DisposeTopicNotification(&event_c); + }); } -void NT_DestroyEntryListenerPoller(NT_EntryListenerPoller poller) { - nt::DestroyEntryListenerPoller(poller); +NT_TopicListenerPoller NT_CreateTopicListenerPoller(NT_Inst inst) { + return nt::CreateTopicListenerPoller(inst); } -NT_EntryListener NT_AddPolledEntryListener(NT_EntryListenerPoller poller, +void NT_DestroyTopicListenerPoller(NT_TopicListenerPoller poller) { + nt::DestroyTopicListenerPoller(poller); +} + +NT_TopicListener NT_AddPolledTopicListener(NT_TopicListenerPoller poller, const char* prefix, size_t prefix_len, - unsigned int flags) { - return nt::AddPolledEntryListener(poller, {prefix, prefix_len}, flags); + unsigned int mask) { + std::string_view p{prefix, prefix_len}; + return nt::AddPolledTopicListener(poller, {{p}}, mask); } -NT_EntryListener NT_AddPolledEntryListenerSingle(NT_EntryListenerPoller poller, - NT_Entry entry, - unsigned int flags) { - return nt::AddPolledEntryListener(poller, entry, flags); +NT_TopicListener NT_AddPolledTopicListenerMultiple( + NT_TopicListenerPoller poller, const NT_String* prefixes, + size_t prefixes_len, unsigned int mask) { + wpi::SmallVector p; + p.reserve(prefixes_len); + for (size_t i = 0; i < prefixes_len; ++i) { + p.emplace_back(prefixes[i].str, prefixes[i].len); + } + return nt::AddPolledTopicListener(poller, p, mask); } -struct NT_EntryNotification* NT_PollEntryListener(NT_EntryListenerPoller poller, - size_t* len) { - auto arr_cpp = nt::PollEntryListener(poller); - return ConvertToC(arr_cpp, len); +NT_TopicListener NT_AddPolledTopicListenerSingle(NT_TopicListenerPoller poller, + NT_Topic topic, + unsigned int mask) { + return nt::AddPolledTopicListener(poller, topic, mask); } -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(arr_cpp, len); +struct NT_TopicNotification* NT_ReadTopicListenerQueue( + NT_TopicListenerPoller poller, size_t* len) { + auto arr_cpp = nt::ReadTopicListenerQueue(poller); + return ConvertToC(arr_cpp, len); } -void NT_CancelPollEntryListener(NT_EntryListenerPoller poller) { - nt::CancelPollEntryListener(poller); +void NT_RemoveTopicListener(NT_TopicListener topic_listener) { + nt::RemoveTopicListener(topic_listener); } -void NT_RemoveEntryListener(NT_EntryListener entry_listener) { - nt::RemoveEntryListener(entry_listener); +NT_ValueListener NT_AddValueListener(NT_Handle subentry, unsigned int mask, + void* data, + NT_ValueListenerCallback callback) { + return nt::AddValueListener(subentry, mask, [=](auto& event) { + NT_ValueNotification event_c; + ConvertToC(event, &event_c); + callback(data, &event_c); + DisposeValueNotification(&event_c); + }); } -NT_Bool NT_WaitForEntryListenerQueue(NT_Inst inst, double timeout) { - return nt::WaitForEntryListenerQueue(inst, timeout); +NT_ValueListenerPoller NT_CreateValueListenerPoller(NT_Inst inst) { + return nt::CreateValueListenerPoller(inst); +} + +void NT_DestroyValueListenerPoller(NT_ValueListenerPoller poller) { + nt::DestroyValueListenerPoller(poller); +} + +NT_ValueListener NT_AddPolledValueListener(NT_ValueListenerPoller poller, + NT_Handle subentry, + unsigned int mask) { + return nt::AddPolledValueListener(poller, subentry, mask); +} + +struct NT_ValueNotification* NT_ReadValueListenerQueue( + NT_ValueListenerPoller poller, size_t* len) { + auto arr_cpp = nt::ReadValueListenerQueue(poller); + return ConvertToC(arr_cpp, len); +} + +void NT_RemoveValueListener(NT_ValueListener value_listener) { + nt::RemoveValueListener(value_listener); } NT_ConnectionListener NT_AddConnectionListener( @@ -389,183 +529,16 @@ NT_ConnectionListener NT_AddPolledConnectionListener( return nt::AddPolledConnectionListener(poller, immediate_notify); } -struct NT_ConnectionNotification* NT_PollConnectionListener( +struct NT_ConnectionNotification* NT_ReadConnectionListenerQueue( NT_ConnectionListenerPoller poller, size_t* len) { - auto arr_cpp = nt::PollConnectionListener(poller); + auto arr_cpp = nt::ReadConnectionListenerQueue(poller); return ConvertToC(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(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_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(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(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; - } - - // convert result - *result_len = result.size(); - char* result_cstr; - ConvertToC(result, &result_cstr); - return result_cstr; -} - -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; - } - - *timed_out = cpp_timed_out; - // convert result - *result_len = result.size(); - char* result_cstr; - ConvertToC(result, &result_cstr); - return result_cstr; -} - -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> 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( - wpi::safe_malloc(values_v.size() * sizeof(NT_Value*))); // NOLINT - for (size_t i = 0; i < values_v.size(); ++i) { - values[i] = static_cast(wpi::safe_malloc(sizeof(NT_Value))); - ConvertToC(*values_v[i], values[i]); - } - return values; -} - /* * Client/Server Functions */ @@ -587,34 +560,21 @@ void NT_StopLocal(NT_Inst inst) { } 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) { + nt::StartClient3(inst); } -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> 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) { + nt::StartClient4(inst); } void NT_StopClient(NT_Inst inst) { @@ -647,8 +607,8 @@ void NT_StopDSClient(NT_Inst inst) { 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,36 +624,16 @@ struct NT_ConnectionInfo* NT_GetConnections(NT_Inst inst, size_t* count) { return ConvertToC(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); -} - /* * Utility Functions */ uint64_t NT_Now(void) { - return wpi::Now(); + return nt::Now(); +} + +void NT_SetNow(uint64_t timestamp) { + nt::SetNow(timestamp); } NT_Logger NT_AddLogger(NT_Inst inst, void* data, NT_LogFunc func, @@ -722,45 +662,38 @@ NT_Logger NT_AddPolledLogger(NT_LoggerPoller poller, unsigned int min_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); +struct NT_LogMessage* NT_ReadLoggerQueue(NT_LoggerPoller poller, size_t* len) { + auto arr_cpp = nt::ReadLoggerQueue(poller); return ConvertToC(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(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 +709,13 @@ void NT_DisposeValue(NT_Value* value) { } 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 +729,10 @@ void NT_InitString(NT_String* str) { 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,26 +743,37 @@ void NT_DisposeConnectionInfoArray(NT_ConnectionInfo* arr, size_t count) { 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_DisposeTopicNotificationArray(NT_TopicNotification* arr, size_t count) { for (size_t i = 0; i < count; i++) { - DisposeEntryNotification(&arr[i]); + DisposeTopicNotification(&arr[i]); } std::free(arr); } -void NT_DisposeEntryNotification(NT_EntryNotification* info) { - DisposeEntryNotification(info); +void NT_DisposeTopicNotification(NT_TopicNotification* info) { + DisposeTopicNotification(info); +} + +void NT_DisposeValueNotificationArray(NT_ValueNotification* arr, size_t count) { + for (size_t i = 0; i < count; i++) { + DisposeValueNotification(&arr[i]); + } + std::free(arr); +} + +void NT_DisposeValueNotification(NT_ValueNotification* info) { + DisposeValueNotification(info); } void NT_DisposeConnectionNotificationArray(NT_ConnectionNotification* arr, @@ -851,62 +800,37 @@ void NT_DisposeLogMessage(NT_LogMessage* info) { 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); -} - /* 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(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(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(wpi::safe_malloc(size * sizeof(int64_t))); + return retVal; +} + +float* NT_AllocateFloatArray(size_t size) { + float* retVal = static_cast(wpi::safe_malloc(size * sizeof(float))); + return retVal; +} + double* NT_AllocateDoubleArray(size_t size) { double* retVal = static_cast(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(wpi::safe_malloc(size * sizeof(NT_String))); @@ -916,99 +840,25 @@ struct NT_String* NT_AllocateStringArray(size_t size) { 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 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 +876,26 @@ NT_Bool NT_GetValueBoolean(const struct NT_Value* value, uint64_t* last_change, 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 +919,16 @@ char* NT_GetValueString(const struct NT_Value* value, uint64_t* last_change, 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(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(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 +946,34 @@ NT_Bool* NT_GetValueBooleanArray(const struct NT_Value* value, 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( + 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( + 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 +1006,4 @@ NT_String* NT_GetValueStringArray(const struct NT_Value* value, 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 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(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(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( - 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 d56a113ab8..f444277bb6 100644 --- a/ntcore/src/main/native/cpp/ntcore_cpp.cpp +++ b/ntcore/src/main/native/cpp/ntcore_cpp.cpp @@ -4,21 +4,33 @@ #include +#include #include #include #include +#include #include #include "Handle.h" #include "InstanceImpl.h" #include "Log.h" -#include "WireDecoder.h" -#include "WireEncoder.h" +#include "Types_internal.h" #include "ntcore.h" +static std::atomic_bool gNowSet{false}; +static std::atomic 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 */ @@ -42,7 +54,7 @@ void DestroyInstance(NT_Inst inst) { NT_Inst GetInstanceFromHandle(NT_Handle handle) { Handle h{handle}; auto type = h.GetType(); - if (type >= Handle::kConnectionListener && type <= Handle::kRpcCallPoller) { + if (type >= Handle::kConnectionListener && type < Handle::kTypeMax) { return Handle(h.GetInst(), 0, Handle::kInstance); } @@ -54,821 +66,556 @@ NT_Inst GetInstanceFromHandle(NT_Handle handle) { */ 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 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 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) { - 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) { - 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) { - 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 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 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 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 GetTopics(NT_Inst inst, std::string_view prefix, + wpi::span types) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->localStorage.GetTopics(prefix, types); + } else { + return {}; + } +} + +std::vector 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 GetTopicInfo(NT_Inst inst, std::string_view prefix, + wpi::span 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 {}; + } +} + +void SetTopicProperties(NT_Topic topic, const wpi::json& properties) { + if (auto ii = InstanceImpl::GetTyped(topic, Handle::kTopic)) { + ii->localStorage.SetTopicProperties(topic, properties); + } +} + +NT_Subscriber Subscribe(NT_Topic topic, NT_Type type, std::string_view typeStr, + wpi::span 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, + wpi::span 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, + wpi::span 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, + wpi::span 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, + wpi::span prefixes, + wpi::span 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 callback, - unsigned int flags) { - int i = Handle{inst}.GetTypedInst(Handle::kInstance); - auto ii = InstanceImpl::Get(i); - if (i < 0 || !ii) { - return 0; - } - - unsigned int uid = ii->storage.AddListener(prefix, callback, flags); - return Handle(i, uid, Handle::kEntryListener); -} - -NT_EntryListener AddEntryListener( - NT_Entry entry, - std::function 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; - } - - 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 PollEntryListener( - NT_EntryListenerPoller poller) { - Handle handle{poller}; - int id = handle.GetTypedIndex(Handle::kEntryListenerPoller); - auto ii = InstanceImpl::Get(handle.GetInst()); - if (id < 0 || !ii) { +NT_TopicListener AddTopicListener( + NT_Inst inst, wpi::span prefixes, unsigned int mask, + std::function callback) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->localStorage.AddTopicListener(prefixes, mask, + std::move(callback)); + } else { return {}; } - - return ii->entry_notifier.Poll(static_cast(id)); } -std::vector 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) { +NT_TopicListener AddTopicListener( + NT_Handle handle, unsigned int mask, + std::function callback) { + if (auto ii = InstanceImpl::GetTyped(handle, Handle::kTopic)) { + return ii->localStorage.AddTopicListener(handle, mask, std::move(callback)); + } else { return {}; } - - return ii->entry_notifier.Poll(static_cast(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; +NT_TopicListenerPoller CreateTopicListenerPoller(NT_Inst inst) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->localStorage.CreateTopicListenerPoller(); + } else { + return {}; } - - 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; +void DestroyTopicListenerPoller(NT_TopicListenerPoller poller) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kTopicListenerPoller)) { + ii->localStorage.DestroyTopicListenerPoller(poller); } - - 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; +NT_TopicListener AddPolledTopicListener( + NT_TopicListenerPoller poller, wpi::span prefixes, + unsigned int mask) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kTopicListenerPoller)) { + return ii->localStorage.AddPolledTopicListener(poller, prefixes, mask); + } else { + return {}; + } +} + +NT_TopicListener AddPolledTopicListener(NT_TopicListenerPoller poller, + NT_Handle handle, unsigned int mask) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kTopicListenerPoller)) { + return ii->localStorage.AddPolledTopicListener(poller, handle, mask); + } else { + return {}; + } +} + +std::vector ReadTopicListenerQueue( + NT_TopicListenerPoller poller) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kTopicListenerPoller)) { + return ii->localStorage.ReadTopicListenerQueue(poller); + } else { + return {}; + } +} + +void RemoveTopicListener(NT_TopicListener listener) { + if (auto ii = InstanceImpl::GetTyped(listener, Handle::kTopicListener)) { + return ii->localStorage.RemoveTopicListener(listener); + } +} + +NT_ValueListener AddValueListener( + NT_Handle subentry, unsigned int mask, + std::function callback) { + if (auto ii = InstanceImpl::GetHandle(subentry)) { + return ii->localStorage.AddValueListener(subentry, mask, + std::move(callback)); + } else { + return {}; + } +} + +NT_ValueListenerPoller CreateValueListenerPoller(NT_Inst inst) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->localStorage.CreateValueListenerPoller(); + } else { + return {}; + } +} + +void DestroyValueListenerPoller(NT_ValueListenerPoller poller) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kValueListenerPoller)) { + ii->localStorage.DestroyValueListenerPoller(poller); + } +} + +NT_ValueListener AddPolledValueListener(NT_ValueListenerPoller poller, + NT_Handle subentry, unsigned int mask) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kValueListenerPoller)) { + return ii->localStorage.AddPolledValueListener(poller, subentry, mask); + } else { + return {}; + } +} + +std::vector ReadValueListenerQueue( + NT_ValueListenerPoller poller) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kValueListenerPoller)) { + return ii->localStorage.ReadValueListenerQueue(poller); + } else { + return {}; + } +} + +void RemoveValueListener(NT_ValueListener listener) { + if (auto ii = InstanceImpl::GetTyped(listener, Handle::kValueListener)) { + return ii->localStorage.RemoveValueListener(listener); } - return ii->entry_notifier.WaitForQueue(timeout); } NT_ConnectionListener AddConnectionListener( NT_Inst inst, std::function callback, bool immediate_notify) { - int i = Handle{inst}.GetTypedInst(Handle::kInstance); - auto ii = InstanceImpl::Get(i); - if (!ii) { - return 0; + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->connectionList.AddListener(std::move(callback), + immediate_notify); + } else { + return {}; } - - 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; + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->connectionList.CreateListenerPoller(); + } else { + return {}; } - - 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; + if (auto ii = + InstanceImpl::GetTyped(poller, Handle::kConnectionListenerPoller)) { + return ii->connectionList.DestroyListenerPoller(poller); } - - 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; + if (auto ii = + InstanceImpl::GetTyped(poller, Handle::kConnectionListenerPoller)) { + return ii->connectionList.AddPolledListener(poller, immediate_notify); + } else { + return {}; } - - unsigned int uid = ii->dispatcher.AddPolledListener(id, immediate_notify); - return Handle(i, uid, Handle::kConnectionListener); } -std::vector PollConnectionListener( +std::vector ReadConnectionListenerQueue( NT_ConnectionListenerPoller poller) { - Handle handle{poller}; - int id = handle.GetTypedIndex(Handle::kConnectionListenerPoller); - auto ii = InstanceImpl::Get(handle.GetInst()); - if (id < 0 || !ii) { + if (auto ii = + InstanceImpl::GetTyped(poller, Handle::kConnectionListenerPoller)) { + return ii->connectionList.ReadListenerQueue(poller); + } else { return {}; } - - return ii->connection_notifier.Poll(static_cast(id)); } -std::vector 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) { - return {}; +void RemoveConnectionListener(NT_ConnectionListener listener) { + if (auto ii = InstanceImpl::GetTyped(listener, Handle::kConnectionListener)) { + return ii->connectionList.RemoveListener(listener); } - - return ii->connection_notifier.Poll(static_cast(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; +int64_t Now() { + if (gNowSet) { + return gNowTime; } - - ii->connection_notifier.CancelPoll(id); -} - -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; - } - - 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; - } - return ii->connection_notifier.WaitForQueue(timeout); -} - -/* - * Remote Procedure Call Functions - */ - -void CreateRpc(NT_Entry entry, std::string_view def, - std::function 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) { - 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; - } - - 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 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(id)); -} - -std::vector 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(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) { - 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; - } - - 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> values) { - WireEncoder enc(0x0300); - for (auto& value : values) { - enc.WriteValue(*value); - } - return std::string{enc.ToStringView()}; -} - -std::vector> UnpackRpcValues( - std::string_view packed, wpi::span types) { - wpi::raw_mem_istream is(packed.data(), packed.size()); - wpi::Logger logger; - WireDecoder dec(is, 0x0300, logger); - std::vector> vec; - for (auto type : types) { - auto item = dec.ReadValue(type); - if (!item) { - return std::vector>(); - } - vec.emplace_back(std::move(item)); - } - return vec; -} - -uint64_t Now() { 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); + } +} + /* * Data Logger Functions */ NT_DataLogger StartEntryDataLog(NT_Inst inst, wpi::log::DataLog& log, std::string_view prefix, std::string_view logPrefix) { - 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.StartDataLog(log, prefix, logPrefix); + } else { return 0; } - - return Handle(i, ii->storage.StartDataLog(log, prefix, logPrefix), - Handle::kDataLogger); } void StopEntryDataLog(NT_DataLogger logger) { - Handle handle{logger}; - int id = handle.GetTypedIndex(Handle::kDataLogger); - auto ii = InstanceImpl::Get(handle.GetInst()); - if (id < 0 || !ii) { - return; + if (auto ii = InstanceImpl::GetTyped(logger, Handle::kDataLogger)) { + ii->localStorage.StopDataLog(logger); } - - ii->storage.StopDataLog(id); } NT_ConnectionDataLogger StartConnectionDataLog(NT_Inst inst, wpi::log::DataLog& log, std::string_view name) { - int i = Handle{inst}.GetTypedInst(Handle::kInstance); - auto ii = InstanceImpl::Get(i); - if (!ii) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->connectionList.StartDataLog(log, name); + } else { return 0; } - - return Handle(i, ii->dispatcher.StartDataLog(log, name), - Handle::kConnectionDataLogger); } void StopConnectionDataLog(NT_ConnectionDataLogger logger) { - Handle handle{logger}; - int id = handle.GetTypedIndex(Handle::kConnectionDataLogger); - auto ii = InstanceImpl::Get(handle.GetInst()); - if (id < 0 || !ii) { - return; + if (auto ii = InstanceImpl::GetTyped(logger, Handle::kConnectionDataLogger)) { + ii->connectionList.StopDataLog(logger); } - - ii->dispatcher.StopDataLog(id); } /* @@ -876,349 +623,217 @@ void StopConnectionDataLog(NT_ConnectionDataLogger logger) { */ void SetNetworkIdentity(NT_Inst inst, std::string_view name) { - auto ii = InstanceImpl::Get(Handle{inst}.GetTypedInst(Handle::kInstance)); - if (!ii) { - return; + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + ii->SetIdentity(name); } - - 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) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + ii->StartClient3(); } - - 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) { + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + ii->StartClient4(); } - - ii->dispatcher.SetServer(server_name, port); - ii->dispatcher.StartClient(); -} - -void StartClient( - NT_Inst inst, - wpi::span> 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> servers) { - 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()) { + std::vector> serversCopy; + serversCopy.reserve(servers.size()); + for (auto&& server : servers) { + serversCopy.emplace_back(std::string{server.first}, server.second); + } + client->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)) { + if (auto client = ii->GetClient()) { + std::vector> servers; + servers.reserve(5); - ii->dispatcher.SetServerTeam(team, port); + // 10.te.am.2 + servers.emplace_back( + fmt::format("10.{}.{}.2", static_cast(team / 100), + static_cast(team % 100)), + port); + + // 172.22.11.2 + servers.emplace_back("172.22.11.2", port); + + // roboRIO--FRC.local + servers.emplace_back(fmt::format("roboRIO-{}-FRC.local", team), port); + + // roboRIO--FRC.lan + servers.emplace_back(fmt::format("roboRIO-{}-FRC.lan", team), port); + + // roboRIO--FRC.frc-field.local + servers.emplace_back(fmt::format("roboRIO-{}-FRC.frc-field.local", team), + port); + + client->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 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 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 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 func, unsigned int minLevel, unsigned int maxLevel) { - int i = Handle{inst}.GetTypedInst(Handle::kInstance); - auto ii = InstanceImpl::Get(i); - if (!ii) { - return 0; + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + if (minLevel < ii->logger.min_level()) { + ii->logger.set_min_level(minLevel); + } + return ii->logger_impl.Add(std::move(func), minLevel, maxLevel); + } else { + return {}; } - - 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; + if (auto ii = InstanceImpl::GetTyped(inst, Handle::kInstance)) { + return ii->logger_impl.CreatePoller(); + } else { + return {}; } - - 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; + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kLoggerPoller)) { + ii->logger_impl.DestroyPoller(poller); } - - 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 PollLogger(NT_LoggerPoller poller) { - Handle handle{poller}; - int id = handle.GetTypedIndex(Handle::kLoggerPoller); - auto ii = InstanceImpl::Get(handle.GetInst()); - if (id < 0 || !ii) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kLoggerPoller)) { + if (min_level < ii->logger.min_level()) { + ii->logger.set_min_level(min_level); + } + return ii->logger_impl.AddPolled(poller, min_level, max_level); + } else { return {}; } - - return ii->logger_impl.Poll(static_cast(id)); } -std::vector 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) { +std::vector ReadLoggerQueue(NT_LoggerPoller poller) { + if (auto ii = InstanceImpl::GetTyped(poller, Handle::kLoggerPoller)) { + return ii->logger_impl.ReadQueue(poller); + } else { return {}; } - - return ii->logger_impl.Poll(static_cast(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; - } - - 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; + if (auto ii = InstanceImpl::GetTyped(logger, Handle::kLogger)) { + ii->logger_impl.Remove(logger); + ii->logger.set_min_level(ii->logger_impl.GetMinLevel()); } - - 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_test.cpp b/ntcore/src/main/native/cpp/ntcore_test.cpp index 64bde688db..ee537e3bd9 100644 --- a/ntcore/src/main/native/cpp/ntcore_test.cpp +++ b/ntcore/src/main/native/cpp/ntcore_test.cpp @@ -19,23 +19,22 @@ struct NT_String* NT_GetStringForTesting(const char* string, int* struct_size) { return str; } -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(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(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 @@ struct NT_Value* NT_GetValueStringArrayForTesting(uint64_t last_change, } // 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(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( - 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( - wpi::safe_calloc(1, sizeof(NT_RpcDefinition))); - def->version = version; - nt::ConvertToC(name, &def->name); - def->num_params = num_params; - def->params = static_cast( - 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( - 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(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/ConnectionListener.h b/ntcore/src/main/native/include/networktables/ConnectionListener.h new file mode 100644 index 0000000000..191d910c1d --- /dev/null +++ b/ntcore/src/main/native/include/networktables/ConnectionListener.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 +#include + +#include + +#include "ntcore_cpp.h" + +namespace nt { + +class NetworkTableInstance; + +/** + * Connection listener. This calls back to a callback function when a connection + * change occurs. + */ +class ConnectionListener final { + public: + ConnectionListener() = default; + + /** + * Create a listener for connection changes. + * + * @param inst Instance + * @param immediateNotify if notification should be immediately created for + * existing connections + * @param listener Listener function + */ + ConnectionListener( + NetworkTableInstance inst, bool immediateNotify, + std::function listener); + + ConnectionListener(const ConnectionListener&) = delete; + ConnectionListener& operator=(const ConnectionListener&) = delete; + ConnectionListener(ConnectionListener&& rhs); + ConnectionListener& operator=(ConnectionListener&& rhs); + ~ConnectionListener(); + + explicit operator bool() const { return m_handle != 0; } + + /** + * Gets the native handle. + * + * @return Handle + */ + NT_ConnectionListener GetHandle() const { return m_handle; } + + private: + NT_ConnectionListener m_handle{0}; +}; + +/** + * A connection listener. This queues connection notifications. Code using + * the listener must periodically call readQueue() to read the notifications. + */ +class ConnectionListenerPoller final { + public: + ConnectionListenerPoller() = default; + + /** + * Construct a connection listener poller. + * + * @param inst Instance + */ + explicit ConnectionListenerPoller(NetworkTableInstance inst); + + ConnectionListenerPoller(const ConnectionListenerPoller&) = delete; + ConnectionListenerPoller& operator=(const ConnectionListenerPoller&) = delete; + ConnectionListenerPoller(ConnectionListenerPoller&& rhs); + ConnectionListenerPoller& operator=(ConnectionListenerPoller&& rhs); + ~ConnectionListenerPoller(); + + explicit operator bool() const { return m_handle != 0; } + + /** + * Gets the native handle. + * + * @return Handle + */ + NT_ConnectionListenerPoller GetHandle() const { return m_handle; } + + /** + * Create a connection listener. + * + * @param immediateNotify if notification should be immediately created for + * existing connections + * @return Listener handle + */ + NT_ConnectionListener Add(bool immediateNotify); + + /** + * Remove a connection listener. + * + * @param listener Listener handle + */ + void Remove(NT_ConnectionListener listener); + + /** + * Read connection notifications. + * + * @return Connection notifications since the previous call to readQueue() + */ + std::vector ReadQueue(); + + private: + NT_ConnectionListenerPoller m_handle{0}; +}; + +} // namespace nt + +#include "ConnectionListener.inc" diff --git a/ntcore/src/main/native/include/networktables/ConnectionListener.inc b/ntcore/src/main/native/include/networktables/ConnectionListener.inc new file mode 100644 index 0000000000..8f652b05e3 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/ConnectionListener.inc @@ -0,0 +1,77 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include + +#include + +#include "networktables/ConnectionListener.h" +#include "networktables/NetworkTableInstance.h" +#include "ntcore_cpp.h" + +namespace nt { + +inline ConnectionListener::ConnectionListener( + NetworkTableInstance inst, bool immediateNotify, + std::function listener) + : m_handle{ + AddConnectionListener(inst.GetHandle(), listener, immediateNotify)} {} + +inline ConnectionListener::ConnectionListener(ConnectionListener&& rhs) + : m_handle(rhs.m_handle) { + rhs.m_handle = 0; +} + +inline ConnectionListener& ConnectionListener::operator=( + ConnectionListener&& rhs) { + std::swap(m_handle, rhs.m_handle); + return *this; +} + +inline ConnectionListener::~ConnectionListener() { + if (m_handle != 0) { + nt::RemoveConnectionListener(m_handle); + } +} + +inline ConnectionListenerPoller::ConnectionListenerPoller( + NetworkTableInstance inst) + : m_handle(nt::CreateConnectionListenerPoller(inst.GetHandle())) {} + +inline ConnectionListenerPoller::ConnectionListenerPoller( + ConnectionListenerPoller&& rhs) + : m_handle(rhs.m_handle) { + rhs.m_handle = 0; +} + +inline ConnectionListenerPoller& ConnectionListenerPoller::operator=( + ConnectionListenerPoller&& rhs) { + std::swap(m_handle, rhs.m_handle); + return *this; +} + +inline ConnectionListenerPoller::~ConnectionListenerPoller() { + if (m_handle != 0) { + nt::DestroyConnectionListenerPoller(m_handle); + } +} + +inline NT_ConnectionListener ConnectionListenerPoller::Add( + bool immediateNotify) { + return nt::AddPolledConnectionListener(m_handle, immediateNotify); +} + +inline void ConnectionListenerPoller::Remove(NT_ConnectionListener listener) { + nt::RemoveConnectionListener(listener); +} + +inline std::vector +ConnectionListenerPoller::ReadQueue() { + return nt::ReadConnectionListenerQueue(m_handle); +} + +} // namespace nt 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 bbf5e42d2a..0000000000 --- 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 0000000000..792e6fdd07 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/GenericEntry.h @@ -0,0 +1,482 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include +#include + +#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 GetRaw(wpi::span 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 instead of std::vector + * because std::vector is special-cased in C++. 0 is false, any + * non-zero value is true. + */ + std::vector GetBooleanArray(wpi::span 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 GetIntegerArray( + wpi::span 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 GetFloatArray(wpi::span 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 GetDoubleArray( + wpi::span 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 GetStringArray( + wpi::span 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 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(wpi::span 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 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 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(wpi::span 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(wpi::span 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 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 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(wpi::span 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(wpi::span 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(wpi::span 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(wpi::span 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(wpi::span 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(wpi::span 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 0000000000..afb4eccff5 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/GenericEntry.inc @@ -0,0 +1,213 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include +#include + +#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 GenericSubscriber::GetRaw( + wpi::span defaultValue) const { + return ::nt::GetRaw(m_subHandle, defaultValue); +} + +inline std::vector GenericSubscriber::GetBooleanArray( + wpi::span defaultValue) const { + return ::nt::GetBooleanArray(m_subHandle, defaultValue); +} + +inline std::vector GenericSubscriber::GetIntegerArray( + wpi::span defaultValue) const { + return ::nt::GetIntegerArray(m_subHandle, defaultValue); +} + +inline std::vector GenericSubscriber::GetFloatArray( + wpi::span defaultValue) const { + return ::nt::GetFloatArray(m_subHandle, defaultValue); +} + +inline std::vector GenericSubscriber::GetDoubleArray( + wpi::span defaultValue) const { + return ::nt::GetDoubleArray(m_subHandle, defaultValue); +} + +inline std::vector GenericSubscriber::GetStringArray( + wpi::span defaultValue) const { + return ::nt::GetStringArray(m_subHandle, defaultValue); +} + +inline std::vector 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(wpi::span value, + int64_t time) { + return nt::SetRaw(m_pubHandle, value, time); +} + +inline bool GenericPublisher::SetBooleanArray(wpi::span value, + int64_t time) { + return SetEntryValue(m_pubHandle, Value::MakeBooleanArray(value, time)); +} + +inline bool GenericPublisher::SetBooleanArray(wpi::span value, + int64_t time) { + return nt::SetBooleanArray(m_pubHandle, value, time); +} + +inline bool GenericPublisher::SetIntegerArray(wpi::span value, + int64_t time) { + return nt::SetIntegerArray(m_pubHandle, value, time); +} + +inline bool GenericPublisher::SetFloatArray(wpi::span value, + int64_t time) { + return nt::SetFloatArray(m_pubHandle, value, time); +} + +inline bool GenericPublisher::SetDoubleArray(wpi::span value, + int64_t time) { + return nt::SetDoubleArray(m_pubHandle, value, time); +} + +inline bool GenericPublisher::SetStringArray(wpi::span 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( + wpi::span defaultValue) { + return nt::SetDefaultRaw(m_pubHandle, defaultValue); +} + +inline bool GenericPublisher::SetDefaultBooleanArray( + wpi::span defaultValue) { + return nt::SetDefaultBooleanArray(m_pubHandle, defaultValue); +} + +inline bool GenericPublisher::SetDefaultIntegerArray( + wpi::span defaultValue) { + return nt::SetDefaultIntegerArray(m_pubHandle, defaultValue); +} + +inline bool GenericPublisher::SetDefaultFloatArray( + wpi::span defaultValue) { + return nt::SetDefaultFloatArray(m_pubHandle, defaultValue); +} + +inline bool GenericPublisher::SetDefaultDoubleArray( + wpi::span defaultValue) { + return nt::SetDefaultDoubleArray(m_pubHandle, defaultValue); +} + +inline bool GenericPublisher::SetDefaultStringArray( + wpi::span 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 0000000000..face5bc960 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/MultiSubscriber.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 + +#include + +#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, + wpi::span prefixes, + wpi::span options = {}); + + 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 0000000000..2c2bbb28e0 --- /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, wpi::span prefixes, + wpi::span 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 8ff265b46f..01ce6cc6aa 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 #include #include +#include #include #include "networktables/NetworkTable.h" -#include "networktables/NetworkTableEntry.h" -#include "networktables/NetworkTableValue.h" +#include "networktables/Topic.h" namespace nt { @@ -26,27 +25,16 @@ class NTSendableBuilder : public wpi::SendableBuilder { * * @param func function */ - virtual void SetUpdateTable(std::function func) = 0; + virtual void SetUpdateTable(wpi::unique_function 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()> getter, - std::function)> 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 3a04e45222..50e16b3a32 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTable.h +++ b/ntcore/src/main/native/include/networktables/NetworkTable.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_NETWORKTABLE_H_ -#define NTCORE_NETWORKTABLES_NETWORKTABLE_H_ +#pragma once #include #include @@ -17,13 +16,23 @@ #include #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 @@ class NetworkTable final { std::string m_path; mutable wpi::mutex m_mutex; mutable wpi::StringMap m_entries; - std::vector m_listeners; struct private_init {}; friend class NetworkTableInstance; @@ -94,7 +102,7 @@ class NetworkTable final { * 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 @@ class NetworkTable final { 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 @@ -191,6 +247,23 @@ class NetworkTable final { */ 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 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 GetTopics(int types = 0) const; + /** * Gets all keys in the table (not including sub-tables). * @@ -229,39 +302,6 @@ class NetworkTable final { */ 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 * @@ -466,7 +506,7 @@ class NetworkTable final { * @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, wpi::span value); /** * Gets the current value in the table, setting it if it does not exist. @@ -475,7 +515,8 @@ class NetworkTable final { * @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, + wpi::span defaultValue); /** * Returns the raw value (byte array) the key maps to. If the key does not @@ -489,7 +530,8 @@ class NetworkTable final { * @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 GetRaw(std::string_view key, + wpi::span defaultValue) const; /** * Put a value in the table @@ -498,7 +540,7 @@ class NetworkTable final { * @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); + 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 @@ class NetworkTable final { * @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 defaultValue); + bool SetDefaultValue(std::string_view key, const Value& defaultValue); /** * Gets the value associated with a key as an object @@ -517,7 +558,7 @@ class NetworkTable final { * @return the value associated with the given key, or nullptr if the key * does not exist */ - std::shared_ptr 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 "/". @@ -525,29 +566,6 @@ class NetworkTable final { * @return The path (e.g "", "/foo"). */ std::string_view GetPath() const; - - /** - * Save table values to a file. The file format used is identical to - * that used for SavePersistent. - * - * @param filename filename - * @return error string, or nullptr if successful - */ - const char* SaveEntries(std::string_view filename) const; - - /** - * Load table values from a file. The file format used is identical to - * that used for SavePersistent / LoadPersistent. - * - * @param filename filename - * @param warn callback function for warnings - * @return error string, or nullptr if successful - */ - const char* LoadEntries( - std::string_view filename, - std::function warn); }; } // 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 c6c9c86a59..12b7d5ed04 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableEntry.h +++ b/ntcore/src/main/native/include/networktables/NetworkTableEntry.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_NETWORKTABLEENTRY_H_ -#define NTCORE_NETWORKTABLES_NETWORKTABLEENTRY_H_ +#pragma once #include @@ -13,26 +12,32 @@ #include #include +#include #include #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 @@ class NetworkTableEntry final { * 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,14 @@ class NetworkTableEntry final { * * @return Entry last change time */ - uint64_t GetLastChange() const; - - /** - * Gets combined information about the entry. - * - * @return Entry information - */ - EntryInfo GetInfo() const; + int64_t GetLastChange() 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 GetValue() const; + Value GetValue() const; /** * Gets the entry's value as a boolean. If the entry does not exist or is of @@ -127,6 +127,24 @@ class NetworkTableEntry final { */ 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 +170,7 @@ class NetworkTableEntry final { * @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 GetRaw(wpi::span defaultValue) const; /** * Gets the entry's value as a boolean array. If the entry does not exist @@ -171,7 +189,7 @@ class NetworkTableEntry final { std::vector GetBooleanArray(wpi::span 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 +197,21 @@ class NetworkTableEntry final { * * @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 instead of std::vector - * because std::vector is special-cased in C++. 0 is false, any - * non-zero value is true. */ - std::vector GetBooleanArray( - std::initializer_list defaultValue) const; + std::vector GetIntegerArray( + wpi::span 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 GetFloatArray(wpi::span defaultValue) const; /** * Gets the entry's value as a double array. If the entry does not exist @@ -200,19 +226,6 @@ class NetworkTableEntry final { std::vector GetDoubleArray( wpi::span 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 GetDoubleArray( - std::initializer_list 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. @@ -227,25 +240,22 @@ class NetworkTableEntry final { wpi::span 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 GetStringArray( - std::initializer_list defaultValue) const; + std::vector 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); + bool SetDefaultValue(const Value& defaultValue); /** * Sets the entry's value if it does not exist. @@ -255,6 +265,22 @@ class NetworkTableEntry final { */ 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. * @@ -277,7 +303,7 @@ class NetworkTableEntry final { * @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(wpi::span defaultValue); /** * Sets the entry's value if it does not exist. @@ -293,7 +319,15 @@ class NetworkTableEntry final { * @param defaultValue the default value to set * @return False if the entry exists with a different type */ - bool SetDefaultBooleanArray(std::initializer_list defaultValue); + bool SetDefaultIntegerArray(wpi::span 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(wpi::span defaultValue); /** * Sets the entry's value if it does not exist. @@ -303,14 +337,6 @@ class NetworkTableEntry final { */ bool SetDefaultDoubleArray(wpi::span 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::initializer_list defaultValue); - /** * Sets the entry's value if it does not exist. * @@ -319,234 +345,138 @@ class NetworkTableEntry final { */ bool SetDefaultStringArray(wpi::span 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 defaultValue); - /** * Sets the entry's value. * * @param value the value to set * @return False if the entry exists with a different type */ - bool SetValue(std::shared_ptr 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 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 value); + bool SetRaw(wpi::span 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 value); + bool SetBooleanArray(wpi::span 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 value); + bool SetBooleanArray(wpi::span 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 value); + bool SetIntegerArray(wpi::span 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 value); + bool SetFloatArray(wpi::span 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 value); + bool SetDoubleArray(wpi::span 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 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); - - /** - * 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 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 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 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 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 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 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 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 value); + bool SetStringArray(wpi::span 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,55 +497,23 @@ class NetworkTableEntry final { 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 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 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 @@ -638,5 +536,3 @@ class NetworkTableEntry final { } // 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 e7deb51733..35648b1740 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 #include @@ -11,6 +10,8 @@ #include #include "networktables/NetworkTableEntry.h" +#include "ntcore_cpp.h" +#include "ntcore_cpp_types.h" namespace nt { @@ -39,307 +40,208 @@ inline unsigned int NetworkTableEntry::GetFlags() const { 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 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 NetworkTableEntry::GetRaw( + wpi::span defaultValue) const { + return nt::GetRaw(m_handle, defaultValue); } inline std::vector NetworkTableEntry::GetBooleanArray( wpi::span 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()}; + return nt::GetBooleanArray(m_handle, defaultValue); } -inline std::vector NetworkTableEntry::GetBooleanArray( - std::initializer_list defaultValue) const { - return GetBooleanArray({defaultValue.begin(), defaultValue.end()}); +inline std::vector NetworkTableEntry::GetIntegerArray( + wpi::span defaultValue) const { + return nt::GetIntegerArray(m_handle, defaultValue); +} + +inline std::vector NetworkTableEntry::GetFloatArray( + wpi::span defaultValue) const { + return nt::GetFloatArray(m_handle, defaultValue); } inline std::vector NetworkTableEntry::GetDoubleArray( wpi::span 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 NetworkTableEntry::GetDoubleArray( - std::initializer_list defaultValue) const { - return GetDoubleArray({defaultValue.begin(), defaultValue.end()}); + return nt::GetDoubleArray(m_handle, defaultValue); } inline std::vector NetworkTableEntry::GetStringArray( wpi::span 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()}; + return nt::GetStringArray(m_handle, defaultValue); } -inline std::vector NetworkTableEntry::GetStringArray( - std::initializer_list defaultValue) const { - return GetStringArray({defaultValue.begin(), defaultValue.end()}); +inline std::vector NetworkTableEntry::ReadQueue() { + return nt::ReadQueueValue(m_handle); } -inline bool NetworkTableEntry::SetDefaultValue(std::shared_ptr 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( + wpi::span defaultValue) { + return nt::SetDefaultRaw(m_handle, defaultValue); } inline bool NetworkTableEntry::SetDefaultBooleanArray( wpi::span defaultValue) { - return SetDefaultEntryValue(m_handle, Value::MakeBooleanArray(defaultValue)); + return nt::SetDefaultBooleanArray(m_handle, defaultValue); } -inline bool NetworkTableEntry::SetDefaultBooleanArray( - std::initializer_list defaultValue) { - return SetDefaultEntryValue(m_handle, Value::MakeBooleanArray(defaultValue)); +inline bool NetworkTableEntry::SetDefaultIntegerArray( + wpi::span defaultValue) { + return nt::SetDefaultIntegerArray(m_handle, defaultValue); +} + +inline bool NetworkTableEntry::SetDefaultFloatArray( + wpi::span defaultValue) { + return nt::SetDefaultFloatArray(m_handle, defaultValue); } inline bool NetworkTableEntry::SetDefaultDoubleArray( wpi::span defaultValue) { - return SetDefaultEntryValue(m_handle, Value::MakeDoubleArray(defaultValue)); -} - -inline bool NetworkTableEntry::SetDefaultDoubleArray( - std::initializer_list value) { - return SetDefaultEntryValue(m_handle, Value::MakeDoubleArray(value)); + return nt::SetDefaultDoubleArray(m_handle, defaultValue); } inline bool NetworkTableEntry::SetDefaultStringArray( wpi::span defaultValue) { - return SetDefaultEntryValue(m_handle, Value::MakeStringArray(defaultValue)); + return nt::SetDefaultStringArray(m_handle, defaultValue); } -inline bool NetworkTableEntry::SetDefaultStringArray( - std::initializer_list defaultValue) { - return SetDefaultEntryValue(m_handle, Value::MakeStringArray(defaultValue)); -} - -inline bool NetworkTableEntry::SetValue(std::shared_ptr 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 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 value) { - return SetEntryValue(m_handle, Value::MakeBooleanArray(value)); +inline bool NetworkTableEntry::SetRaw(wpi::span value, + int64_t time) { + return nt::SetRaw(m_handle, value, time); } -inline bool NetworkTableEntry::SetBooleanArray(wpi::span value) { - return SetEntryValue(m_handle, Value::MakeBooleanArray(value)); +inline bool NetworkTableEntry::SetBooleanArray(wpi::span value, + int64_t time) { + return SetEntryValue(m_handle, Value::MakeBooleanArray(value, time)); } -inline bool NetworkTableEntry::SetBooleanArray( - std::initializer_list value) { - return SetEntryValue(m_handle, Value::MakeBooleanArray(value)); +inline bool NetworkTableEntry::SetBooleanArray(wpi::span value, + int64_t time) { + return nt::SetBooleanArray(m_handle, value, time); } -inline bool NetworkTableEntry::SetDoubleArray(wpi::span value) { - return SetEntryValue(m_handle, Value::MakeDoubleArray(value)); +inline bool NetworkTableEntry::SetIntegerArray(wpi::span value, + int64_t time) { + return nt::SetIntegerArray(m_handle, value, time); } -inline bool NetworkTableEntry::SetDoubleArray( - std::initializer_list value) { - return SetEntryValue(m_handle, Value::MakeDoubleArray(value)); +inline bool NetworkTableEntry::SetFloatArray(wpi::span value, + int64_t time) { + return nt::SetFloatArray(m_handle, value, time); +} + +inline bool NetworkTableEntry::SetDoubleArray(wpi::span value, + int64_t time) { + return nt::SetDoubleArray(m_handle, value, time); } inline bool NetworkTableEntry::SetStringArray( - wpi::span value) { - return SetEntryValue(m_handle, Value::MakeStringArray(value)); -} - -inline bool NetworkTableEntry::SetStringArray( - std::initializer_list value) { - return SetEntryValue(m_handle, Value::MakeStringArray(value)); -} - -inline void NetworkTableEntry::ForceSetValue(std::shared_ptr 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 value) { - SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value)); -} - -inline void NetworkTableEntry::ForceSetBooleanArray( - std::initializer_list value) { - SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value)); -} - -inline void NetworkTableEntry::ForceSetBooleanArray( - wpi::span value) { - SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value)); -} - -inline void NetworkTableEntry::ForceSetBooleanArray( - std::initializer_list value) { - SetEntryTypeValue(m_handle, Value::MakeBooleanArray(value)); -} - -inline void NetworkTableEntry::ForceSetDoubleArray( - wpi::span value) { - SetEntryTypeValue(m_handle, Value::MakeDoubleArray(value)); -} - -inline void NetworkTableEntry::ForceSetDoubleArray( - std::initializer_list value) { - SetEntryTypeValue(m_handle, Value::MakeDoubleArray(value)); -} - -inline void NetworkTableEntry::ForceSetStringArray( - wpi::span value) { - SetEntryTypeValue(m_handle, Value::MakeStringArray(value)); -} - -inline void NetworkTableEntry::ForceSetStringArray( - std::initializer_list value) { - SetEntryTypeValue(m_handle, Value::MakeStringArray(value)); + wpi::span 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 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 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 891a46acf2..638665c3db 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.h +++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.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_NETWORKTABLEINSTANCE_H_ -#define NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_H_ +#pragma once #include #include @@ -21,6 +20,19 @@ namespace nt { +class BooleanArrayTopic; +class BooleanTopic; +class DoubleArrayTopic; +class DoubleTopic; +class FloatArrayTopic; +class FloatTopic; +class IntegerArrayTopic; +class IntegerTopic; +class RawTopic; +class StringArrayTopic; +class StringTopic; +class Topic; + /** * NetworkTables Instance. * @@ -51,9 +63,8 @@ class NetworkTableInstance final { 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 +84,14 @@ class NetworkTableInstance final { }; /** - * 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. @@ -124,6 +140,204 @@ class NetworkTableInstance final { */ 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 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 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 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 GetTopics(std::string_view prefix, + wpi::span 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 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 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 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 GetTopicInfo(std::string_view prefix, + wpi::span types); + /** * Gets the entry for a key. * @@ -132,34 +346,6 @@ class NetworkTableInstance final { */ 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 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 GetEntryInfo(std::string_view prefix, - unsigned int types) const; - /** * Gets the table with the specified key. * @@ -168,51 +354,6 @@ class NetworkTableInstance final { */ std::shared_ptr GetTable(std::string_view key) const; - /** - * Deletes ALL keys in ALL subtables (except persistent values). - * Use with caution! - */ - void DeleteAllEntries(); - - /** - * @{ - * @name Entry Listener Functions - */ - - /** - * Add a listener for all entries starting with a certain prefix. - * - * @param prefix UTF-8 string prefix - * @param callback listener to add - * @param flags EntryListenerFlags bitmask - * @return Listener handle - */ - NT_EntryListener AddEntryListener( - std::string_view prefix, - std::function callback, - unsigned int flags) const; - - /** - * 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 @@ -236,37 +377,6 @@ class NetworkTableInstance final { */ 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 - * 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 WaitForConnectionListenerQueue(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. - * - * @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(double timeout); - /** @} */ /** @@ -311,11 +421,13 @@ class NetworkTableInstance final { * 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 +435,16 @@ class NetworkTableInstance final { 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. */ - void StartClient(); + void StartClient3(); /** - * Starts a client using the specified server and port - * - * @param server_name server name (UTF-8 string, null terminated) - * @param port port to communicate over + * Starts a NT4 client. Use SetServer or SetServerTeam to set the server name + * and port. */ - 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> 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 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(); /** * Stops the client if it is running. @@ -372,15 +455,15 @@ class NetworkTableInstance final { * 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> servers); @@ -390,28 +473,28 @@ class NetworkTableInstance final { * 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 servers, - unsigned int port = kDefaultPort); + 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 +502,10 @@ class NetworkTableInstance final { 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. @@ -451,60 +532,6 @@ class NetworkTableInstance final { /** @} */ - /** - * @{ - * @name 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. - * - * @param filename filename - * @return error string, or nullptr if successful - */ - const char* SavePersistent(std::string_view filename) const; - - /** - * 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 filename filename - * @param warn callback function for warnings - * @return error string, or nullptr if successful - */ - const char* LoadPersistent( - std::string_view filename, - std::function warn); - - /** - * 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 - * @return error string, or nullptr if successful - */ - const char* SaveEntries(std::string_view filename, - std::string_view prefix) const; - - /** - * 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 - * @param warn callback function for warnings - * @return error string, or nullptr if successful - */ - const char* LoadEntries( - std::string_view filename, std::string_view prefix, - std::function warn); - - /** @} */ - /** * @{ * @name Data Logger Functions @@ -578,18 +605,6 @@ class NetworkTableInstance final { */ 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); - /** @} */ /** @@ -613,5 +628,3 @@ class NetworkTableInstance final { } // 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 88986bcc55..f2a088b7f9 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc +++ b/ntcore/src/main/native/include/networktables/NetworkTableInstance.inc @@ -2,15 +2,14 @@ // Open Source 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 #include #include #include "networktables/NetworkTableInstance.h" -#include "ntcore_cpp.h" +#include "networktables/Topic.h" namespace nt { @@ -37,51 +36,57 @@ inline NT_Inst NetworkTableInstance::GetHandle() const { return m_handle; } +inline std::vector NetworkTableInstance::GetTopics() { + auto handles = ::nt::GetTopics(m_handle, "", 0); + return {handles.begin(), handles.end()}; +} + +inline std::vector NetworkTableInstance::GetTopics( + std::string_view prefix) { + auto handles = ::nt::GetTopics(m_handle, prefix, 0); + return {handles.begin(), handles.end()}; +} + +inline std::vector 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 NetworkTableInstance::GetTopics( + std::string_view prefix, wpi::span types) { + auto handles = ::nt::GetTopics(m_handle, prefix, types); + return {handles.begin(), handles.end()}; +} + +inline std::vector NetworkTableInstance::GetTopicInfo() { + return ::nt::GetTopicInfo(m_handle, "", 0); +} + +inline std::vector NetworkTableInstance::GetTopicInfo( + std::string_view prefix) { + return ::nt::GetTopicInfo(m_handle, prefix, 0); +} + +inline std::vector NetworkTableInstance::GetTopicInfo( + std::string_view prefix, unsigned int types) { + return ::nt::GetTopicInfo(m_handle, prefix, types); +} + +inline std::vector NetworkTableInstance::GetTopicInfo( + std::string_view prefix, wpi::span 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 NetworkTableInstance::GetEntries( - std::string_view prefix, unsigned int types) { - std::vector entries; - for (auto entry : ::nt::GetEntries(m_handle, prefix, types)) { - entries.emplace_back(entry); - } - return entries; -} - -inline std::vector NetworkTableInstance::GetEntryInfo( - std::string_view prefix, unsigned int types) const { - return ::nt::GetEntryInfo(m_handle, prefix, types); -} - -inline void NetworkTableInstance::DeleteAllEntries() { - ::nt::DeleteAllEntries(m_handle); -} - -inline void NetworkTableInstance::RemoveEntryListener( - NT_EntryListener entry_listener) { - ::nt::RemoveEntryListener(entry_listener); -} - -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); } @@ -100,31 +105,21 @@ inline void NetworkTableInstance::StopLocal() { 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() { + ::nt::StartClient3(m_handle); } -inline void NetworkTableInstance::StartClient(const char* server_name, - unsigned int port) { - ::nt::StartClient(m_handle, server_name, port); -} - -inline void NetworkTableInstance::StartClient( - wpi::span> 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() { + ::nt::StartClient4(m_handle); } inline void NetworkTableInstance::StopClient() { @@ -154,8 +149,8 @@ inline void NetworkTableInstance::StopDSClient() { ::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 { @@ -171,28 +166,6 @@ inline bool NetworkTableInstance::IsConnected() const { return ::nt::IsConnected(m_handle); } -inline const char* NetworkTableInstance::SavePersistent( - std::string_view filename) const { - return ::nt::SavePersistent(m_handle, filename); -} - -inline const char* NetworkTableInstance::LoadPersistent( - std::string_view filename, - std::function warn) { - return ::nt::LoadPersistent(m_handle, filename, warn); -} - -inline const char* NetworkTableInstance::SaveEntries( - std::string_view filename, std::string_view prefix) const { - return ::nt::SaveEntries(m_handle, filename, prefix); -} - -inline const char* NetworkTableInstance::LoadEntries( - std::string_view filename, std::string_view prefix, - std::function warn) { - return ::nt::LoadEntries(m_handle, filename, prefix, warn); -} - inline NT_DataLogger NetworkTableInstance::StartEntryDataLog( wpi::log::DataLog& log, std::string_view prefix, std::string_view logPrefix) { @@ -223,10 +196,4 @@ inline void NetworkTableInstance::RemoveLogger(NT_Logger logger) { ::nt::RemoveLogger(logger); } -inline bool NetworkTableInstance::WaitForLoggerQueue(double timeout) { - return ::nt::WaitForLoggerQueue(m_handle, timeout); -} - } // namespace nt - -#endif // NTCORE_NETWORKTABLES_NETWORKTABLEINSTANCE_INC_ diff --git a/ntcore/src/main/native/include/networktables/NetworkTableType.h b/ntcore/src/main/native/include/networktables/NetworkTableType.h index d7681f25b0..4b60454365 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 @@ enum class NetworkTableType { 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 fcb9cb59e0..80632b3017 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableValue.h +++ b/ntcore/src/main/native/include/networktables/NetworkTableValue.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_NETWORKTABLEVALUE_H_ -#define NTCORE_NETWORKTABLES_NETWORKTABLEVALUE_H_ +#pragma once #include @@ -31,8 +30,10 @@ class Value final { 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 +50,39 @@ class Value final { 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; } /** * @{ @@ -81,6 +103,20 @@ class Value final { */ 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. * @@ -102,13 +138,6 @@ class Value final { */ 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. * @@ -116,6 +145,20 @@ class Value final { */ 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. * @@ -147,6 +190,26 @@ class Value final { return m_val.data.v_boolean != 0; } + /** + * 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. * @@ -164,7 +227,7 @@ class Value final { */ 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 +235,9 @@ class Value final { * * @return The raw value. */ - std::string_view GetRaw() const { + wpi::span 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}; } /** @@ -197,6 +250,26 @@ class Value final { 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. + */ + wpi::span 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. + */ + wpi::span 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. * @@ -214,7 +287,7 @@ class Value final { */ wpi::span GetStringArray() const { assert(m_val.type == NT_STRING_ARRAY); - return m_string_array; + return *static_cast*>(m_storage.get()); } /** @} */ @@ -232,9 +305,37 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeBoolean(bool value, uint64_t time = 0) { - auto val = std::make_shared(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 +347,9 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeDouble(double value, uint64_t time = 0) { - auto val = std::make_shared(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 +361,12 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeString(std::string_view value, - uint64_t time = 0) { - auto val = std::make_shared(NT_STRING, time, private_init()); - val->m_string = value; - val->m_val.data.v_string.str = const_cast(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(value); + val.m_val.data.v_string.str = const_cast(data->c_str()); + val.m_val.data.v_string.len = data->size(); + val.m_storage = std::move(data); return val; } @@ -279,11 +380,12 @@ class Value final { */ template ::value>::type> - static std::shared_ptr MakeString(T&& value, uint64_t time = 0) { - auto val = std::make_shared(NT_STRING, time, private_init()); - val->m_string = std::forward(value); - val->m_val.data.v_string.str = const_cast(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::forward(value)); + val.m_val.data.v_string.str = const_cast(data->c_str()); + val.m_val.data.v_string.len = data->size(); + val.m_storage = std::move(data); return val; } @@ -295,12 +397,13 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeRaw(std::string_view value, - uint64_t time = 0) { - auto val = std::make_shared(NT_RAW, time, private_init()); - val->m_string = value; - val->m_val.data.v_raw.str = const_cast(val->m_string.c_str()); - val->m_val.data.v_raw.len = val->m_string.size(); + static Value MakeRaw(wpi::span value, int64_t time = 0) { + Value val{NT_RAW, time, private_init{}}; + auto data = + std::make_shared>(value.begin(), value.end()); + val.m_val.data.v_raw.data = const_cast(data->data()); + val.m_val.data.v_raw.size = data->size(); + val.m_storage = std::move(data); return val; } @@ -312,47 +415,14 @@ class Value final { * time) * @return The entry value */ - template ::value>::type> - static std::shared_ptr MakeRaw(T&& value, uint64_t time = 0) { - auto val = std::make_shared(NT_RAW, time, private_init()); - val->m_string = std::forward(value); - val->m_val.data.v_raw.str = const_cast(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 MakeRpc(std::string_view value, - uint64_t time = 0) { - auto val = std::make_shared(NT_RPC, time, private_init()); - val->m_string = value; - val->m_val.data.v_raw.str = const_cast(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 - static std::shared_ptr MakeRpc(T&& value, uint64_t time = 0) { - auto val = std::make_shared(NT_RPC, time, private_init()); - val->m_string = std::forward(value); - val->m_val.data.v_raw.str = const_cast(val->m_string.c_str()); - val->m_val.data.v_raw.len = val->m_string.size(); + template >::value>::type> + static Value MakeRaw(T&& value, int64_t time = 0) { + Value val{NT_RAW, time, private_init{}}; + auto data = std::make_shared>(std::forward(value)); + val.m_val.data.v_raw.data = const_cast(data->data()); + val.m_val.data.v_raw.size = data->size(); + val.m_storage = std::move(data); return val; } @@ -364,8 +434,7 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeBooleanArray(wpi::span value, - uint64_t time = 0); + static Value MakeBooleanArray(wpi::span value, int64_t time = 0); /** * Creates a boolean array entry value. @@ -375,8 +444,8 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeBooleanArray( - std::initializer_list value, uint64_t time = 0) { + static Value MakeBooleanArray(std::initializer_list value, + int64_t time = 0) { return MakeBooleanArray(wpi::span(value.begin(), value.end()), time); } @@ -388,8 +457,7 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeBooleanArray(wpi::span value, - uint64_t time = 0); + static Value MakeBooleanArray(wpi::span value, int64_t time = 0); /** * Creates a boolean array entry value. @@ -399,11 +467,58 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeBooleanArray( - std::initializer_list value, uint64_t time = 0) { + static Value MakeBooleanArray(std::initializer_list value, + int64_t time = 0) { return MakeBooleanArray(wpi::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 + */ + static Value MakeIntegerArray(wpi::span 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 value, + int64_t time = 0) { + return MakeIntegerArray(wpi::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 + */ + static Value MakeFloatArray(wpi::span 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 value, + int64_t time = 0) { + return MakeFloatArray(wpi::span(value.begin(), value.end()), time); + } + /** * Creates a double array entry value. * @@ -412,8 +527,7 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeDoubleArray(wpi::span value, - uint64_t time = 0); + static Value MakeDoubleArray(wpi::span value, int64_t time = 0); /** * Creates a double array entry value. @@ -423,8 +537,8 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeDoubleArray( - std::initializer_list value, uint64_t time = 0) { + static Value MakeDoubleArray(std::initializer_list value, + int64_t time = 0) { return MakeDoubleArray(wpi::span(value.begin(), value.end()), time); } @@ -436,8 +550,8 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeStringArray( - wpi::span value, uint64_t time = 0); + static Value MakeStringArray(wpi::span value, + int64_t time = 0); /** * Creates a string array entry value. @@ -447,8 +561,8 @@ class Value final { * time) * @return The entry value */ - static std::shared_ptr MakeStringArray( - std::initializer_list value, uint64_t time = 0) { + static Value MakeStringArray(std::initializer_list value, + int64_t time = 0) { return MakeStringArray(wpi::span(value.begin(), value.end()), time); } @@ -462,19 +576,16 @@ class Value final { * * @note This function moves the values out of the vector. */ - static std::shared_ptr MakeStringArray( - std::vector&& value, uint64_t time = 0); + static Value MakeStringArray(std::vector&& 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 m_string_array; + std::shared_ptr m_storage; }; bool operator==(const Value& lhs, const Value& rhs); @@ -489,5 +600,3 @@ inline bool operator!=(const Value& lhs, const Value& rhs) { 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 9c6e9f90c1..0000000000 --- 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 -#include - -#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 5e25b0410e..0000000000 --- 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 -#include - -#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 180234f386..0000000000 --- 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 -#include -#include - -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 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 cc1113ea61..0000000000 --- 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 -#include -#include - -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 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 0000000000..7f9da69c57 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/Topic.h @@ -0,0 +1,384 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include +#include +#include +#include + +#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 + */ + void SetProperties(const wpi::json& properties); + + /** + * Gets combined information about the topic. + * + * @return Topic information + */ + TopicInfo GetInfo() const; + + /** + * Create a new subscriber to the topic. + * + *

The subscriber is only active as long as the returned object + * is not destroyed. + * + * @param options subscribe options + * @return subscriber + */ + [[nodiscard]] GenericSubscriber GenericSubscribe( + wpi::span options = {}); + + /** + * Create a new subscriber to the topic. + * + *

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, wpi::span options = {}); + + /** + * 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, wpi::span options = {}); + + /** + * Create a new publisher to the topic, with type string and initial + * properties. + * + * The publisher is only active as long as the returned object + * is not destroyed. + * + * @note It is not possible to publish two different data types to the same + * topic. Conflicts between publishers are typically resolved by the + * server on a first-come, first-served basis. Any published values that + * do not match the topic's data type are dropped (ignored). To determine + * if the data type matches, use the appropriate Topic functions. + * + * @param typeString type string + * @param properties JSON properties + * @param options publish options + * @return publisher + */ + [[nodiscard]] GenericPublisher GenericPublishEx( + std::string_view typeString, const wpi::json& properties, + wpi::span options = {}); + + /** + * 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( + wpi::span options = {}); + + /** + * 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, wpi::span options = {}); + + /** + * Equality operator. Returns true if both instances refer to the same + * native handle. + */ + bool operator==(const Topic& oth) const { return m_handle == oth.m_handle; } + + /** Inequality operator. */ + bool operator!=(const Topic& oth) const { return !(*this == oth); } + + 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 pubscriber. */ +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 0000000000..62b65285cf --- /dev/null +++ b/ntcore/src/main/native/include/networktables/Topic.inc @@ -0,0 +1,109 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#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(::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 void Topic::SetProperties(const wpi::json& properties) { + ::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) { + 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) { + 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/TopicListener.h b/ntcore/src/main/native/include/networktables/TopicListener.h new file mode 100644 index 0000000000..e4f738d0d4 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/TopicListener.h @@ -0,0 +1,246 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include +#include + +#include + +#include "ntcore_cpp.h" + +namespace nt { + +class MultiSubscriber; +class NetworkTableEntry; +class NetworkTableInstance; +class Subscriber; +class Topic; + +/** + * Flag values for use with topic listeners. + * + * The flags are a bitmask and must be OR'ed together to indicate the + * combination of events desired to be received. + * + * The constants kPublish, kUnpublish, and kProperties represent different + * events that can occur to entries. + * + * @ingroup ntcore_cpp_api + */ +struct TopicListenerFlags { + TopicListenerFlags() = delete; + + /** + * Initial listener addition. + * Set this flag to receive immediate notification of entries matching the + * flag criteria (generally only useful when combined with kPublish). + */ + static constexpr unsigned int kImmediate = NT_TOPIC_NOTIFY_IMMEDIATE; + + /** + * Newly published topic. + * + * Set this flag to receive a notification when a topic is initially + * published. + */ + static constexpr unsigned int kPublish = NT_TOPIC_NOTIFY_PUBLISH; + + /** + * Topic has no more publishers. + * + * Set this flag to receive a notification when a topic has no more + * publishers. + */ + static constexpr unsigned int kUnpublish = NT_TOPIC_NOTIFY_UNPUBLISH; + + /** + * Topic's properties changed. + * + * Set this flag to receive a notification when an topic's properties change. + */ + static constexpr unsigned int kProperties = NT_TOPIC_NOTIFY_PROPERTIES; +}; + +/** + * Topic change listener. This calls back to a callback function when a topic + * change matching the specified mask occurs. + */ +class TopicListener final { + public: + TopicListener() = default; + + /** + * Create a listener for changes to topics with names that start with any of + * the given prefixes. + * + * @param inst Instance + * @param prefixes Topic name string prefixes + * @param mask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + TopicListener(NetworkTableInstance inst, + wpi::span prefixes, unsigned int mask, + std::function listener); + + /** + * Create a listener for changes on a particular topic. + * + * @param topic Topic + * @param mask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + TopicListener(Topic topic, unsigned int mask, + std::function listener); + + /** + * Create a listener for topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + TopicListener(const Subscriber& subscriber, unsigned int mask, + std::function listener); + + /** + * Create a listener for topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + TopicListener(const MultiSubscriber& subscriber, unsigned int mask, + std::function listener); + + /** + * Create a listener for topic changes on an entry. + * + * @param entry Entry + * @param mask Bitmask of TopicListenerFlags values + * @param listener Listener function + */ + TopicListener(const NetworkTableEntry& entry, unsigned int mask, + std::function listener); + + TopicListener(const TopicListener&) = delete; + TopicListener& operator=(const TopicListener&) = delete; + TopicListener(TopicListener&& rhs); + TopicListener& operator=(TopicListener&& rhs); + ~TopicListener(); + + explicit operator bool() const { return m_handle != 0; } + + /** + * Gets the native handle. + * + * @return Handle + */ + NT_TopicListener GetHandle() const { return m_handle; } + + private: + NT_TopicListener m_handle{0}; +}; + +/** + * Topic change listener. This queues topic change events matching the specified + * mask. Code using the listener must periodically call readQueue() to read the + * events. + */ +class TopicListenerPoller final { + public: + TopicListenerPoller() = default; + + /** + * Construct a topic listener poller. + * + * @param inst Instance + */ + explicit TopicListenerPoller(NetworkTableInstance inst); + + TopicListenerPoller(const TopicListenerPoller&) = delete; + TopicListenerPoller& operator=(const TopicListenerPoller&) = delete; + TopicListenerPoller(TopicListenerPoller&& rhs); + TopicListenerPoller& operator=(TopicListenerPoller&& rhs); + ~TopicListenerPoller(); + + explicit operator bool() const { return m_handle != 0; } + + /** + * Gets the native handle. + * + * @return Handle + */ + NT_TopicListenerPoller GetHandle() const { return m_handle; } + + /** + * Start listening to changes to a particular topic. + * + * @param prefixes Topic name string prefixes + * @param mask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + NT_TopicListener Add(wpi::span prefixes, + unsigned int mask); + + /** + * Start listening to topic changes for topics with names that start with any + * of the given prefixes. + * + * @param topic Topic + * @param mask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + NT_TopicListener Add(Topic topic, unsigned int mask); + + /** + * Start listening to topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + NT_ValueListener Add(const Subscriber& subscriber, unsigned int mask); + + /** + * Start listening to topic changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + NT_ValueListener Add(const MultiSubscriber& subscriber, unsigned int mask); + + /** + * Start listening to topic changes on an entry. + * + * @param entry Entry + * @param mask Bitmask of TopicListenerFlags values + * @return Listener handle + */ + NT_ValueListener Add(const NetworkTableEntry& entry, unsigned int mask); + + /** + * Remove a listener. + * + * @param listener Listener handle + */ + void Remove(NT_TopicListener listener); + + /** + * Read topic notifications. + * + * @return Topic notifications since the previous call to readQueue() + */ + std::vector ReadQueue(); + + private: + NT_TopicListenerPoller m_handle{0}; +}; + +} // namespace nt + +#include "TopicListener.inc" diff --git a/ntcore/src/main/native/include/networktables/TopicListener.inc b/ntcore/src/main/native/include/networktables/TopicListener.inc new file mode 100644 index 0000000000..9c94566db4 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/TopicListener.inc @@ -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 +#include +#include + +#include + +#include "networktables/MultiSubscriber.h" +#include "networktables/NetworkTableEntry.h" +#include "networktables/NetworkTableInstance.h" +#include "networktables/Topic.h" +#include "networktables/TopicListener.h" +#include "ntcore_cpp.h" + +namespace nt { + +inline TopicListener::TopicListener( + NetworkTableInstance inst, wpi::span prefixes, + unsigned int mask, std::function listener) + : m_handle{AddTopicListener(inst.GetHandle(), prefixes, mask, listener)} {} + +inline TopicListener::TopicListener( + Topic topic, unsigned int mask, + std::function listener) + : m_handle{AddTopicListener(topic.GetHandle(), mask, listener)} {} + +inline TopicListener::TopicListener( + const Subscriber& subscriber, unsigned int mask, + std::function listener) + : m_handle{AddTopicListener(subscriber.GetHandle(), mask, listener)} {} + +inline TopicListener::TopicListener( + const MultiSubscriber& subscriber, unsigned int mask, + std::function listener) + : m_handle{AddTopicListener(subscriber.GetHandle(), mask, listener)} {} + +inline TopicListener::TopicListener( + const NetworkTableEntry& entry, unsigned int mask, + std::function listener) + : m_handle{AddTopicListener(entry.GetHandle(), mask, listener)} {} + +inline TopicListener::TopicListener(TopicListener&& rhs) + : m_handle(rhs.m_handle) { + rhs.m_handle = 0; +} + +inline TopicListener& TopicListener::operator=(TopicListener&& rhs) { + std::swap(m_handle, rhs.m_handle); + return *this; +} + +inline TopicListener::~TopicListener() { + if (m_handle != 0) { + nt::RemoveTopicListener(m_handle); + } +} + +inline TopicListenerPoller::TopicListenerPoller(NetworkTableInstance inst) + : m_handle(nt::CreateTopicListenerPoller(inst.GetHandle())) {} + +inline TopicListenerPoller::TopicListenerPoller(TopicListenerPoller&& rhs) + : m_handle(rhs.m_handle) { + rhs.m_handle = 0; +} + +inline TopicListenerPoller& TopicListenerPoller::operator=( + TopicListenerPoller&& rhs) { + std::swap(m_handle, rhs.m_handle); + return *this; +} + +inline TopicListenerPoller::~TopicListenerPoller() { + if (m_handle != 0) { + nt::DestroyTopicListenerPoller(m_handle); + } +} + +inline NT_TopicListener TopicListenerPoller::Add( + wpi::span prefixes, unsigned int mask) { + return nt::AddPolledTopicListener(m_handle, prefixes, mask); +} + +inline NT_TopicListener TopicListenerPoller::Add(Topic topic, + unsigned int mask) { + return nt::AddPolledTopicListener(m_handle, topic.GetHandle(), mask); +} + +inline NT_TopicListener TopicListenerPoller::Add(const Subscriber& subscriber, + unsigned int mask) { + return nt::AddPolledTopicListener(m_handle, subscriber.GetHandle(), mask); +} + +inline NT_TopicListener TopicListenerPoller::Add( + const MultiSubscriber& subscriber, unsigned int mask) { + return nt::AddPolledTopicListener(m_handle, subscriber.GetHandle(), mask); +} + +inline NT_TopicListener TopicListenerPoller::Add(const NetworkTableEntry& entry, + unsigned int mask) { + return nt::AddPolledTopicListener(m_handle, entry.GetHandle(), mask); +} + +inline void TopicListenerPoller::Remove(NT_TopicListener listener) { + nt::RemoveTopicListener(listener); +} + +inline std::vector TopicListenerPoller::ReadQueue() { + return nt::ReadTopicListenerQueue(m_handle); +} + +} // namespace nt diff --git a/ntcore/src/main/native/include/networktables/ValueListener.h b/ntcore/src/main/native/include/networktables/ValueListener.h new file mode 100644 index 0000000000..e2804c365e --- /dev/null +++ b/ntcore/src/main/native/include/networktables/ValueListener.h @@ -0,0 +1,204 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include + +#include "ntcore_cpp.h" + +namespace nt { + +class MultiSubscriber; +class NetworkTableEntry; +class NetworkTableInstance; +class Subscriber; + +/** + * Flag values for use with value listeners. + * + * The flags are a bitmask and must be OR'ed together to indicate the + * combination of events desired to be received. + * + * 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. + */ +struct ValueListenerFlags { + ValueListenerFlags() = delete; + + /** + * Initial listener addition. + * + * Set this flag to receive immediate notification of the current value. + */ + static constexpr unsigned int kImmediate = NT_VALUE_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. + */ + static constexpr unsigned int kLocal = NT_VALUE_NOTIFY_LOCAL; +}; + +/** + * Value change listener. This calls back to a callback function when a value + * change matching the specified mask occurs. + */ +class ValueListener final { + public: + ValueListener() = default; + + /** + * Create a listener for value changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of ValueListenerFlags values + * @param listener Listener function + */ + ValueListener(const Subscriber& subscriber, unsigned int mask, + std::function listener); + + /** + * Create a listener for value changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of ValueListenerFlags values + * @param listener Listener function + */ + ValueListener(const MultiSubscriber& subscriber, unsigned int mask, + std::function listener); + + /** + * Create a listener for value changes on an entry. + * + * @param entry Entry + * @param mask Bitmask of ValueListenerFlags values + * @param listener Listener function + */ + ValueListener(const NetworkTableEntry& entry, unsigned int mask, + std::function listener); + + /** + * Create a listener for value changes on a subscriber/entry handle. + * + * @param subentry Subscriber/entry handle + * @param mask Bitmask of ValueListenerFlags values + * @param listener Listener function + */ + ValueListener(NT_Handle subentry, unsigned int mask, + std::function listener); + + ValueListener(const ValueListener&) = delete; + ValueListener& operator=(const ValueListener&) = delete; + ValueListener(ValueListener&& rhs); + ValueListener& operator=(ValueListener&& rhs); + ~ValueListener(); + + explicit operator bool() const { return m_handle != 0; } + + /** + * Gets the native handle. + * + * @return Handle + */ + NT_ValueListener GetHandle() const { return m_handle; } + + private: + NT_ValueListener m_handle{0}; +}; + +/** + * Value change listener. This queues value change events matching the specified + * mask. Code using the listener must periodically call readQueue() to read the + * events. + */ +class ValueListenerPoller final { + public: + ValueListenerPoller(); + + /** + * Construct a value listener poller. + * + * @param inst Instance + */ + explicit ValueListenerPoller(NetworkTableInstance inst); + + ValueListenerPoller(const ValueListenerPoller&) = delete; + ValueListenerPoller& operator=(const ValueListenerPoller&) = delete; + ValueListenerPoller(ValueListenerPoller&& rhs); + ValueListenerPoller& operator=(ValueListenerPoller&& rhs); + ~ValueListenerPoller(); + + explicit operator bool() const { return m_handle != 0; } + + /** + * Gets the native handle. + * + * @return Handle + */ + NT_ValueListenerPoller GetHandle() const { return m_handle; } + + /** + * Start listening to value changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of ValueListenerFlags values + * @return Listener handle + */ + NT_ValueListener Add(const Subscriber& subscriber, unsigned int mask); + + /** + * Start listening to value changes on a subscriber. + * + * @param subscriber Subscriber + * @param mask Bitmask of ValueListenerFlags values + * @return Listener handle + */ + NT_ValueListener Add(const MultiSubscriber& subscriber, unsigned int mask); + + /** + * Start listening to value changes on an entry. + * + * @param entry Entry + * @param mask Bitmask of ValueListenerFlags values + * @return Listener handle + */ + NT_ValueListener Add(const NetworkTableEntry& entry, unsigned int mask); + + /** + * Start listening to value changes on a subscriber/entry handle. + * + * @param subentry Subscriber/entry handle + * @param mask Bitmask of ValueListenerFlags values + * @return Listener handle + */ + NT_ValueListener Add(NT_Handle subentry, unsigned int mask); + + /** + * Remove a listener. + * + * @param listener Listener handle + */ + void Remove(NT_ValueListener listener); + + /** + * Reads value listener queue (all value changes since last call). + * + * @param poller poller handle + * @return Array of value notifications. + */ + std::vector ReadQueue(); + + private: + NT_ValueListenerPoller m_handle; +}; + +} // namespace nt + +#include "ValueListener.inc" diff --git a/ntcore/src/main/native/include/networktables/ValueListener.inc b/ntcore/src/main/native/include/networktables/ValueListener.inc new file mode 100644 index 0000000000..6872468e00 --- /dev/null +++ b/ntcore/src/main/native/include/networktables/ValueListener.inc @@ -0,0 +1,103 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 +#include + +#include "networktables/MultiSubscriber.h" +#include "networktables/NetworkTableEntry.h" +#include "networktables/NetworkTableInstance.h" +#include "networktables/Topic.h" +#include "networktables/ValueListener.h" +#include "ntcore_cpp.h" + +namespace nt { + +inline ValueListener::ValueListener( + const Subscriber& subscriber, unsigned int mask, + std::function listener) + : m_handle{AddValueListener(subscriber.GetHandle(), mask, listener)} {} + +inline ValueListener::ValueListener( + const MultiSubscriber& subscriber, unsigned int mask, + std::function listener) + : m_handle{AddValueListener(subscriber.GetHandle(), mask, listener)} {} + +inline ValueListener::ValueListener( + const NetworkTableEntry& entry, unsigned int mask, + std::function listener) + : m_handle{AddValueListener(entry.GetHandle(), mask, listener)} {} + +inline ValueListener::ValueListener( + NT_Handle subentry, unsigned int mask, + std::function listener) + : m_handle{AddValueListener(subentry, mask, listener)} {} + +inline ValueListener::ValueListener(ValueListener&& rhs) + : m_handle(rhs.m_handle) { + rhs.m_handle = 0; +} + +inline ValueListener& ValueListener::operator=(ValueListener&& rhs) { + std::swap(m_handle, rhs.m_handle); + return *this; +} + +inline ValueListener::~ValueListener() { + if (m_handle != 0) { + nt::RemoveValueListener(m_handle); + } +} + +inline ValueListenerPoller::ValueListenerPoller(NetworkTableInstance inst) + : m_handle(nt::CreateValueListenerPoller(inst.GetHandle())) {} + +inline ValueListenerPoller::ValueListenerPoller(ValueListenerPoller&& rhs) + : m_handle(rhs.m_handle) { + rhs.m_handle = 0; +} + +inline ValueListenerPoller& ValueListenerPoller::operator=( + ValueListenerPoller&& rhs) { + std::swap(m_handle, rhs.m_handle); + return *this; +} + +inline ValueListenerPoller::~ValueListenerPoller() { + if (m_handle != 0) { + nt::DestroyValueListenerPoller(m_handle); + } +} + +inline NT_ValueListener ValueListenerPoller::Add(const Subscriber& subscriber, + unsigned int mask) { + return Add(subscriber.GetHandle(), mask); +} + +inline NT_ValueListener ValueListenerPoller::Add( + const MultiSubscriber& subscriber, unsigned int mask) { + return Add(subscriber.GetHandle(), mask); +} + +inline NT_ValueListener ValueListenerPoller::Add(const NetworkTableEntry& entry, + unsigned int mask) { + return Add(entry.GetHandle(), mask); +} + +inline NT_ValueListener ValueListenerPoller::Add(NT_Handle subentry, + unsigned int mask) { + return nt::AddPolledValueListener(m_handle, subentry, mask); +} + +inline void ValueListenerPoller::Remove(NT_ValueListener listener) { + nt::RemoveValueListener(listener); +} + +inline std::vector ValueListenerPoller::ReadQueue() { + return nt::ReadValueListenerQueue(m_handle); +} + +} // namespace nt diff --git a/ntcore/src/main/native/include/ntcore.h b/ntcore/src/main/native/include/ntcore.h index 5cdd47301e..e4ae7f981c 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 1e32dae7aa..a2deb83b58 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 @@ -34,16 +33,23 @@ typedef NT_Handle NT_ConnectionListener; typedef NT_Handle NT_ConnectionListenerPoller; 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_MultiSubscriber; +typedef NT_Handle NT_Topic; +typedef NT_Handle NT_TopicListener; +typedef NT_Handle NT_TopicListenerPoller; +typedef NT_Handle NT_Subscriber; +typedef NT_Handle NT_Publisher; +typedef NT_Handle NT_ValueListener; +typedef NT_Handle NT_ValueListenerPoller; -/** 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 { @@ -55,11 +61,15 @@ enum NT_Type { 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 { @@ -74,27 +84,41 @@ enum NT_LogLevel { 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 */ }; +/** Pub/sub option types */ +enum NT_PubSubOptionType { + NT_PUBSUB_PERIODIC = 1, /* period between transmissions */ + NT_PUBSUB_SENDALL, /* all value changes are sent */ + NT_PUBSUB_TOPICSONLY, /* only send topic changes, no value changes */ + NT_PUBSUB_POLLSTORAGE, /* polling storage for subscription */ + NT_PUBSUB_KEEPDUPLICATES, /* preserve duplicate values */ +}; + +/** Topic notification flags. */ +enum NT_TopicNotifyKind { + NT_TOPIC_NOTIFY_NONE = 0, + NT_TOPIC_NOTIFY_IMMEDIATE = 0x01, /* initial listener addition */ + NT_TOPIC_NOTIFY_PUBLISH = 0x02, /* initially published */ + NT_TOPIC_NOTIFY_UNPUBLISH = 0x04, /* no more publishers */ + NT_TOPIC_NOTIFY_PROPERTIES = 0x08, /* properties changed */ +}; + +/** Value notification flags. */ +enum NT_ValueNotifyKind { + NT_VALUE_NOTIFY_NONE = 0, + NT_VALUE_NOTIFY_IMMEDIATE = 0x01, /* initial listener addition */ + NT_VALUE_NOTIFY_LOCAL = 0x02, /* changed locally */ +}; + /* * Structures */ @@ -119,12 +143,18 @@ struct NT_String { /** 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; @@ -133,6 +163,14 @@ struct NT_Value { double* arr; 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; @@ -140,22 +178,21 @@ struct NT_Value { } data; }; -/** NetworkTables Entry Information */ -struct NT_EntryInfo { - /** Entry handle */ - NT_Entry entry; +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 */ @@ -185,47 +222,28 @@ struct NT_ConnectionInfo { unsigned int protocol_version; }; -/** NetworkTables RPC Version 1 Definition Parameter */ -struct NT_RpcParamDef { - struct NT_String name; - struct NT_Value def_value; -}; - -/** 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 { +/** NetworkTables Topic Notification */ +struct NT_TopicNotification { /** Listener that was triggered. */ - NT_EntryListener listener; + NT_TopicListener listener; - /** Entry handle. */ - NT_Entry entry; + /** Topic info. */ + struct NT_TopicInfo info; - /** Entry name. */ - struct NT_String name; + /** Update flags. */ + unsigned int flags; +}; + +/** NetworkTables Value Notification */ +struct NT_ValueNotification { + /** Listener that was triggered. */ + NT_ValueListener listener; + + /** Topic handle. */ + NT_Topic topic; + + /** Subscriber/entry handle. */ + NT_Handle subentry; /** The new value. */ struct NT_Value value; @@ -267,6 +285,18 @@ struct NT_LogMessage { char* message; }; +/** NetworkTables publish/subscribe option. */ +struct NT_PubSubOption { + /** Option type. */ + enum NT_PubSubOptionType type; + + /** + * Option value. 1 (true) or 0 (false) for immediate and logging options, + * time between updates, in seconds, for periodic option. + */ + double value; +}; + /** * @defgroup ntcore_instance_cfunc Instance Functions * @{ @@ -320,25 +350,6 @@ NT_Inst NT_GetInstanceFromHandle(NT_Handle handle); */ 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. @@ -407,21 +418,6 @@ NT_Bool NT_SetDefaultEntryValue(NT_Entry entry, */ 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. * @@ -439,224 +435,593 @@ void NT_SetEntryFlags(NT_Entry entry, unsigned int flags); 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 + */ +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 + * @param options_len number of elements in options array + * @return Subscriber handle + */ +NT_Subscriber NT_Subscribe(NT_Topic topic, enum NT_Type type, + const char* typeStr, + const struct NT_PubSubOption* options, + size_t options_len); + +/** + * 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 + * @param options_len number of elements in options array + * @return Publisher handle + */ +NT_Publisher NT_Publish(NT_Topic topic, enum NT_Type type, const char* typeStr, + const struct NT_PubSubOption* options, + size_t options_len); + +/** + * 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 + * @param options_len number of elements in options array + * @return Publisher handle + */ +NT_Publisher NT_PublishEx(NT_Topic topic, enum NT_Type type, + const char* typeStr, const char* properties, + const struct NT_PubSubOption* options, + size_t options_len); + +/** + * 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 + * @param options_len number of elements in options array + * @return Entry handle + */ +NT_Entry NT_GetEntryEx(NT_Topic topic, enum NT_Type type, const char* typeStr, + const struct NT_PubSubOption* options, + size_t options_len); + +/** + * 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 inst instance handle + * @param prefixes topic name prefixes + * @param prefixes_len number of elements in prefixes array + * @param options subscriber options + * @param options_len number of elements in options array + * @return subscriber handle + */ +NT_MultiSubscriber NT_SubscribeMultiple(NT_Inst inst, + const struct NT_String* prefixes, + size_t prefixes_len, + const struct NT_PubSubOption* options, + size_t options_len); + +/** + * Unsubscribes a multi-subscriber. + * + * @param sub multi-subscriber handle + */ +void NT_UnsubscribeMultiple(NT_MultiSubscriber sub); + +/** @} */ + +/** + * @defgroup ntcore_topiclistener_cfunc Topic Listener Functions + * @{ + */ + +/** + * Topic listener callback function. * * @param data data pointer provided to callback creation function - * @param event event information + * @param event event info */ -typedef void (*NT_EntryListenerCallback)( - void* data, const struct NT_EntryNotification* event); +typedef void (*NT_TopicListenerCallback)( + void* data, const struct NT_TopicNotification* event); /** - * Add a listener for all entries starting with a certain prefix. + * Create a listener for changes to topics with names that start with + * the given prefix. * - * @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 inst Instance handle + * @param prefix Topic name string prefix + * @param prefix_len Length of topic name string prefix + * @param mask Bitmask of NT_TopicListenerFlags values + * @param data Data passed to callback function + * @param callback Listener function */ -NT_EntryListener NT_AddEntryListener(NT_Inst inst, const char* prefix, - size_t prefix_len, void* data, - NT_EntryListenerCallback callback, - unsigned int flags); +NT_TopicListener NT_AddTopicListener(NT_Inst inst, const char* prefix, + size_t prefix_len, unsigned int mask, + void* data, + NT_TopicListenerCallback callback); /** - * Add a listener for a single entry. + * Create a listener for changes to topics with names that start with any of + * the given prefixes. * - * @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 + * @param inst Instance handle + * @param prefixes Topic name string prefixes + * @param prefixes_len Number of elements in prefixes array + * @param mask Bitmask of NT_TopicListenerFlags values + * @param data Data passed to callback function + * @param callback Listener function */ -NT_EntryListener NT_AddEntryListenerSingle(NT_Entry entry, void* data, - NT_EntryListenerCallback callback, - unsigned int flags); +NT_TopicListener NT_AddTopicListenerMultiple(NT_Inst inst, + const struct NT_String* prefixes, + size_t prefixes_len, + unsigned int mask, void* data, + NT_TopicListenerCallback callback); /** - * Create a entry listener poller. + * Create a listener for changes on a particular topic. + * + * @param topic Topic handle + * @param mask Bitmask of NT_TopicListenerFlags values + * @param data Data passed to callback function + * @param callback Listener function + */ +NT_TopicListener NT_AddTopicListenerSingle(NT_Topic topic, unsigned int mask, + void* data, + NT_TopicListenerCallback callback); + +/** + * Creates a topic 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(). + * poller (using NT_AddPolledTopicListener()) will be stored in the queue and + * must be collected by calling NT_ReadTopicListenerQueue(). + * The returned handle must be destroyed with NT_DestroyTopicListenerPoller(). * * @param inst instance handle * @return poller handle */ -NT_EntryListenerPoller NT_CreateEntryListenerPoller(NT_Inst inst); +NT_TopicListenerPoller NT_CreateTopicListenerPoller(NT_Inst inst); /** - * Destroy a entry listener poller. This will abort any blocked polling + * Destroys a topic 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); +void NT_DestroyTopicListenerPoller(NT_TopicListenerPoller poller); /** - * Create a polled entry listener. - * The caller is responsible for calling NT_PollEntryListener() to poll. + * Read topic notifications. + * + * @param poller poller handle + * @param len length of returned array (output) + * @return Array of topic notifications. Returns NULL and len=0 if no + * notifications since last call. + */ +struct NT_TopicNotification* NT_ReadTopicListenerQueue( + NT_TopicListenerPoller poller, size_t* len); + +/** + * Creates a polled topic listener. + * The caller is responsible for calling NT_ReadTopicListenerQueue() 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 + * @param mask NT_NotifyKind bitmask * @return Listener handle */ -NT_EntryListener NT_AddPolledEntryListener(NT_EntryListenerPoller poller, +NT_TopicListener NT_AddPolledTopicListener(NT_TopicListenerPoller poller, const char* prefix, size_t prefix_len, - unsigned int flags); + unsigned int mask); /** - * Create a polled entry listener. - * The caller is responsible for calling NT_PollEntryListener() to poll. + * Creates a polled topic listener. + * The caller is responsible for calling NT_ReadTopicListenerQueue() to poll. * * @param poller poller handle - * @param entry entry handle - * @param flags NT_NotifyKind bitmask + * @param prefixes array of UTF-8 string prefixes + * @param prefixes_len Length of prefixes array + * @param mask NT_NotifyKind bitmask * @return Listener handle */ -NT_EntryListener NT_AddPolledEntryListenerSingle(NT_EntryListenerPoller poller, - NT_Entry entry, - unsigned int flags); +NT_TopicListener NT_AddPolledTopicListenerMultiple( + NT_TopicListenerPoller poller, const struct NT_String* prefixes, + size_t prefixes_len, unsigned int mask); /** - * Get the next entry listener event. This blocks until the next event occurs. + * Creates a polled topic listener. + * The caller is responsible for calling NT_ReadTopicListenerQueue() to poll. * - * 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 topic topic + * @param mask NT_NotifyKind bitmask + * @return Listener handle + */ +NT_TopicListener NT_AddPolledTopicListenerSingle(NT_TopicListenerPoller poller, + NT_Topic topic, + unsigned int mask); + +/** + * Removes a topic listener. + * + * @param topic_listener Listener handle to remove + */ +void NT_RemoveTopicListener(NT_TopicListener topic_listener); + +/** @} */ + +/** + * @defgroup ntcore_valuelistener_cfunc Value Listener Functions + * @{ + */ + +/** + * Value listener callback function. + * + * @param data data pointer provided to callback creation function + * @param event event info + */ +typedef void (*NT_ValueListenerCallback)( + void* data, const struct NT_ValueNotification* event); + +/** + * Create a listener for value changes on a subscriber. + * + * @param subentry Subscriber/entry + * @param mask Bitmask of NT_ValueListenerFlags values + * @param data Data passed to listener function + * @param callback Listener function + */ +NT_ValueListener NT_AddValueListener(NT_Handle subentry, unsigned int mask, + void* data, + NT_ValueListenerCallback callback); + +/** + * Create a value listener poller. + * + * A poller provides a single queue of poll events. Events linked to this + * poller (using NT_AddPolledValueListener()) will be stored in the queue and + * must be collected by calling NT_ReadValueListenerQueue(). + * The returned handle must be destroyed with NT_DestroyValueListenerPoller(). + * + * @param inst instance handle + * @return poller handle + */ +NT_ValueListenerPoller NT_CreateValueListenerPoller(NT_Inst inst); + +/** + * Destroy a value 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_DestroyValueListenerPoller(NT_ValueListenerPoller poller); + +/** + * Reads value listener queue (all value changes since last call). * * @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). + * @return Array of value notifications. Returns NULL and len=0 if no + * notifications since last call. */ -struct NT_EntryNotification* NT_PollEntryListener(NT_EntryListenerPoller poller, - size_t* len); +struct NT_ValueNotification* NT_ReadValueListenerQueue( + NT_ValueListenerPoller 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. + * Create a polled value listener. + * The caller is responsible for calling NT_ReadValueListenerQueue() to poll. * - * @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). + * @param poller poller handle + * @param subentry subscriber or entry handle + * @param flags NT_NotifyKind bitmask + * @return Listener handle */ -struct NT_EntryNotification* NT_PollEntryListenerTimeout( - NT_EntryListenerPoller poller, size_t* len, double timeout, - NT_Bool* timed_out); +NT_ValueListener NT_AddPolledValueListener(NT_ValueListenerPoller poller, + NT_Handle subentry, + unsigned int flags); /** - * Cancel a PollEntryListener call. This wakes up a call to - * PollEntryListener for this poller and causes it to immediately return - * an empty array. + * Remove a value listener. * - * @param poller poller handle + * @param value_listener Listener handle to remove */ -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_RemoveValueListener(NT_ValueListener value_listener); /** @} */ @@ -731,36 +1096,9 @@ NT_ConnectionListener NT_AddPolledConnectionListener( * if an error occurred (e.g. the instance was invalid or is shutting * down). */ -struct NT_ConnectionNotification* NT_PollConnectionListener( +struct NT_ConnectionNotification* NT_ReadConnectionListenerQueue( NT_ConnectionListenerPoller 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. - * - * @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). - */ -struct NT_ConnectionNotification* NT_PollConnectionListenerTimeout( - NT_ConnectionListenerPoller poller, size_t* len, double timeout, - NT_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 NT_CancelPollConnectionListener(NT_ConnectionListenerPoller poller); - /** * Remove a connection listener. * @@ -768,250 +1106,6 @@ void NT_CancelPollConnectionListener(NT_ConnectionListenerPoller poller); */ 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 - * @return False if timed out, otherwise true. - */ -NT_Bool NT_WaitForConnectionListenerQueue(NT_Inst inst, double timeout); - -/** @} */ - -/** - * @defgroup ntcore_rpc_cfunc Remote Procedure Call Functions - * @{ - */ - -/** - * Remote Procedure Call (RPC) callback function. - * - * @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. - */ -typedef void (*NT_RpcCallback)(void* data, const struct NT_RpcAnswer* call); - -/** - * 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 def_len length of def in bytes - * @param data data pointer to pass to callback function - * @param callback callback function - */ -void NT_CreateRpc(NT_Entry entry, const char* def, size_t def_len, void* data, - NT_RpcCallback 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 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 - */ -NT_RpcCallPoller NT_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 NT_DestroyRpcCallPoller(NT_RpcCallPoller poller); - -/** - * Create a polled RPC entry point. Only valid to use on the server. - * - * The caller is responsible for calling NT_PollRpc() or NT_PollRpcTimeout() - * to poll for servicing incoming RPC calls. - * - * @param entry entry handle of RPC entry - * @param def RPC definition - * @param def_len length of def in bytes - * @param poller poller 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); - /** @} */ /** @@ -1059,10 +1153,12 @@ void NT_StopLocal(NT_Inst inst); * 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. @@ -1072,42 +1168,20 @@ void NT_StartServer(NT_Inst inst, const char* persist_filename, 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 */ -void NT_StartClientNone(NT_Inst inst); +void NT_StartClient3(NT_Inst inst); /** - * 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 */ -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); /** * Stops the client if it is running. @@ -1154,7 +1228,7 @@ void NT_SetServerTeam(NT_Inst inst, unsigned int team, unsigned int port); * 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); @@ -1166,20 +1240,23 @@ void NT_StartDSClient(NT_Inst inst, unsigned int port); 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 @@ -1212,65 +1289,6 @@ 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. - * - * @param inst instance handle - * @param filename filename - * @return error string, or NULL if successful - */ -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)); - -/** @} */ - /** * @defgroup ntcore_utility_cfunc Utility Functions * @{ @@ -1307,12 +1325,16 @@ void NT_DisposeString(struct NT_String* str); 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. @@ -1323,57 +1345,51 @@ void NT_DisposeEntryArray(NT_Entry* arr, size_t count); 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 topic notification 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); - -/** - * Disposes a Rpc Answer structure. - * - * @param answer pointer to the struct 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, +void NT_DisposeTopicNotificationArray(struct NT_TopicNotification* arr, size_t count); /** - * Disposes a single entry notification. + * Disposes a single topic notification. * * @param info pointer to the info to dispose */ -void NT_DisposeEntryNotification(struct NT_EntryNotification* info); +void NT_DisposeTopicNotification(struct NT_TopicNotification* info); + +/** + * Disposes an value notification array. + * + * @param arr pointer to the array to dispose + * @param count number of elements in the array + */ +void NT_DisposeValueNotificationArray(struct NT_ValueNotification* arr, + size_t count); + +/** + * Disposes a single value notification. + * + * @param info pointer to the info to dispose + */ +void NT_DisposeValueNotification(struct NT_ValueNotification* info); /** * Disposes a connection notification array. @@ -1409,12 +1425,25 @@ void NT_DisposeLogMessage(struct NT_LogMessage* info); /** * 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); +/** + * 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(uint64_t timestamp); + /** @} */ /** @@ -1485,30 +1514,7 @@ NT_Logger NT_AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level, * @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); +struct NT_LogMessage* NT_ReadLoggerQueue(NT_LoggerPoller poller, size_t* len); /** * Remove a logger. @@ -1517,19 +1523,6 @@ void NT_CancelPollLogger(NT_LoggerPoller poller); */ 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); - /** @} */ /** @@ -1568,6 +1561,32 @@ char* NT_AllocateCharArray(size_t size); */ 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 @@ -1601,13 +1620,6 @@ struct NT_String* NT_AllocateStringArray(size_t size); */ 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. * @@ -1615,6 +1627,27 @@ void NT_FreeDoubleArray(double* v_double); */ 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. * @@ -1655,13 +1688,37 @@ enum NT_Type NT_GetValueType(const struct NT_Value* value); NT_Bool NT_GetValueBoolean(const struct NT_Value* value, uint64_t* last_change, 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, @@ -1698,8 +1755,8 @@ char* NT_GetValueString(const struct NT_Value* value, uint64_t* last_change, * 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. @@ -1718,6 +1775,40 @@ char* NT_GetValueRaw(const struct NT_Value* value, uint64_t* last_change, NT_Bool* NT_GetValueBooleanArray(const struct NT_Value* value, 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. @@ -1755,325 +1846,6 @@ struct NT_String* NT_GetValueStringArray(const struct NT_Value* value, 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 - * @{ - */ - -/** 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 - */ -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); - -/** @} */ - -/** - * @defgroup ntcore_valuesetters_cfunc Entry Value Setters - * @{ - */ - -/** 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. - * - * @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 - */ -NT_Bool NT_SetEntryBoolean(NT_Entry entry, uint64_t time, NT_Bool v_boolean, - NT_Bool force); - -/** 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. - * - * @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 - */ -NT_Bool NT_SetEntryDouble(NT_Entry entry, uint64_t time, double v_double, - NT_Bool force); - -/** 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. - * - * @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 - */ -NT_Bool NT_SetEntryString(NT_Entry entry, uint64_t time, const char* str, - size_t str_len, NT_Bool force); - -/** 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. - * - * @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 - */ -NT_Bool NT_SetEntryRaw(NT_Entry entry, uint64_t time, const char* raw, - size_t raw_len, NT_Bool force); - -/** 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. - * - * @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 - */ -NT_Bool NT_SetEntryBooleanArray(NT_Entry entry, uint64_t time, const int* arr, - size_t size, NT_Bool force); - -/** 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. - * - * @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 - */ -NT_Bool NT_SetEntryDoubleArray(NT_Entry entry, uint64_t time, const double* arr, - size_t size, NT_Bool force); - -/** 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. - * - * @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 - */ -NT_Bool NT_SetEntryStringArray(NT_Entry entry, uint64_t time, - const struct NT_String* arr, size_t size, - NT_Bool force); - /** @} */ /** @} */ /** @} */ @@ -2082,4 +1854,4 @@ NT_Bool NT_SetEntryStringArray(NT_Entry entry, uint64_t time, } // 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 84ccb9ce5a..35547d5154 100644 --- a/ntcore/src/main/native/include/ntcore_cpp.h +++ b/ntcore/src/main/native/include/ntcore_cpp.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_CPP_H_ -#define NTCORE_NTCORE_CPP_H_ +#pragma once #include @@ -12,13 +11,20 @@ #include #include #include -#include #include #include #include #include "networktables/NetworkTableValue.h" +#include "ntcore_c.h" +#include "ntcore_cpp_types.h" + +namespace wpi { +template +class SmallVectorImpl; +class json; +} // namespace wpi namespace wpi::log { class DataLog; @@ -35,30 +41,33 @@ namespace nt { * @{ */ -/** NetworkTables Entry Information */ -struct EntryInfo { - /** Entry handle */ - NT_Entry entry; +/** NetworkTables Topic Information */ +struct TopicInfo { + /** Topic handle */ + NT_Topic topic{0}; - /** Entry name */ + /** 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); } }; @@ -80,7 +89,7 @@ struct ConnectionInfo { * 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 @@ -98,108 +107,56 @@ struct ConnectionInfo { } }; -/** NetworkTables RPC Version 1 Definition Parameter */ -struct RpcParamDef { - RpcParamDef() = default; - RpcParamDef(std::string_view name_, std::shared_ptr def_value_) - : name(name_), def_value(std::move(def_value_)) {} - - std::string name; - std::shared_ptr 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 params; - std::vector results; -}; - -/** NetworkTables Remote Procedure Call (Server Side) */ -class RpcAnswer { +/** NetworkTables Topic Notification */ +class TopicNotification { 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_)) {} + TopicNotification() = default; + TopicNotification(NT_TopicListener listener_, TopicInfo info_, + unsigned int flags_) + : listener(listener_), info(std::move(info_)), flags(flags_) {} - /** Entry handle. */ - NT_Entry entry{0}; + /** Listener that was triggered. */ + NT_TopicListener listener{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; + /** Topic info. */ + TopicInfo info; /** - * Determines if the native handle is valid. - * @return True if the native handle is valid, false otherwise. + * Notification flags. */ - explicit operator bool() const { return call != 0; } + unsigned int flags{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) { + friend void swap(TopicNotification& first, TopicNotification& 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); + swap(first.listener, second.listener); + swap(first.info, second.info); + swap(first.flags, second.flags); } }; -/** NetworkTables Entry Notification */ -class EntryNotification { +/** NetworkTables Value Notification */ +class ValueNotification { public: - EntryNotification() = default; - EntryNotification(NT_EntryListener listener_, NT_Entry entry_, - std::string_view name_, std::shared_ptr value_, - unsigned int flags_) + ValueNotification() = default; + ValueNotification(NT_ValueListener listener_, NT_Topic topic_, + NT_Handle subentry_, Value value_, unsigned int flags_) : listener(listener_), - entry(entry_), - name(name_), + topic(topic_), + subentry(subentry_), value(std::move(value_)), flags(flags_) {} /** Listener that was triggered. */ - NT_EntryListener listener{0}; + NT_ValueListener listener{0}; - /** Entry handle. */ - NT_Entry entry{0}; + /** Topic handle. */ + NT_Topic topic{0}; - /** Entry name. */ - std::string name; + /** Subscriber/entry handle. */ + NT_Handle subentry{0}; /** The new value. */ - std::shared_ptr value; + Value value; /** * Update flags. For example, NT_NOTIFY_NEW if the key did not previously @@ -207,11 +164,11 @@ class EntryNotification { */ unsigned int flags{0}; - friend void swap(EntryNotification& first, EntryNotification& second) { + friend void swap(ValueNotification& first, ValueNotification& second) { using std::swap; swap(first.listener, second.listener); - swap(first.entry, second.entry); - swap(first.name, second.name); + swap(first.topic, second.topic); + swap(first.subentry, second.subentry); swap(first.value, second.value); swap(first.flags, second.flags); } @@ -280,6 +237,75 @@ class LogMessage { } }; +/** NetworkTables publish/subscribe option. */ +class PubSubOption { + public: + constexpr PubSubOption(NT_PubSubOptionType type, double value) + : type{type}, value{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 (and the immediate flag is false) is 100 ms. This option and + * the immediate option override each other. + * + * @param time time between updates, in seconds + * @return option + */ + static constexpr PubSubOption Periodic(double time) { + return PubSubOption{NT_PUBSUB_PERIODIC, time}; + } + + /** + * If enabled, sends all value changes over the network even if only sent + * periodically. This option defaults to disabled. + * + * @param enabled True to enable, false to disable + * @return option + */ + static constexpr PubSubOption SendAll(bool enabled) { + return PubSubOption{NT_PUBSUB_SENDALL, enabled ? 1.0 : 0.0}; + } + + /** + * If enabled, no value changes are sent over the network. This option + * defaults to disabled. + * + * @param enabled True to enable, false to disable + * @return option + */ + static constexpr PubSubOption TopicsOnly(bool enabled) { + return PubSubOption{NT_PUBSUB_TOPICSONLY, enabled ? 1.0 : 0.0}; + } + + /** + * 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 + */ + static constexpr PubSubOption KeepDuplicates(bool enabled) { + return PubSubOption{NT_PUBSUB_KEEPDUPLICATES, enabled ? 1.0 : 0.0}; + } + + /** + * Polling storage for subscription. Specifies the maximum number of updates + * NetworkTables should store between calls to the subscriber's poll() + * function. Defaults to 1 if logging is false, 20 if logging is true. + * + * @param depth number of entries to save for polling. + * @return option + */ + static constexpr PubSubOption PollStorage(int depth) { + return PubSubOption{NT_PUBSUB_POLLSTORAGE, static_cast(depth)}; + } + + NT_PubSubOptionType type; + double value; +}; + /** * @defgroup ntcore_instance_func Instance Functions * @{ @@ -332,23 +358,6 @@ NT_Inst GetInstanceFromHandle(NT_Handle handle); */ 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 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. @@ -370,10 +379,10 @@ NT_Type GetEntryType(NT_Entry entry); * 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. @@ -381,10 +390,10 @@ uint64_t GetEntryLastChange(NT_Entry entry); * 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 GetEntryValue(NT_Entry entry); +Value GetEntryValue(NT_Handle subentry); /** * Set Default Entry Value @@ -397,7 +406,7 @@ std::shared_ptr GetEntryValue(NT_Entry entry); * @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); +bool SetDefaultEntryValue(NT_Entry entry, const Value& value); /** * Set Entry Value. @@ -409,22 +418,7 @@ bool SetDefaultEntryValue(NT_Entry entry, std::shared_ptr value); * @param value new entry value * @return False on error (type mismatch), True on success */ -bool SetEntryValue(NT_Entry entry, std::shared_ptr 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); +bool SetEntryValue(NT_Entry entry, const Value& value); /** * Set Entry Flags. @@ -443,215 +437,497 @@ void SetEntryFlags(NT_Entry entry, unsigned int flags); 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); - -/** - * 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 - */ -void DeleteAllEntries(NT_Inst inst); - -/** - * Get Entry Information. - * - * 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. - * - * @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. - */ -std::vector GetEntryInfo(NT_Inst inst, std::string_view prefix, - unsigned int types); - -/** - * Get Entry Information. - * - * Returns information about an entry (name, entry type, - * and timestamp of last change to type/value). - * - * @param entry entry handle - * @return Entry information. - */ -EntryInfo GetEntryInfo(NT_Entry entry); +std::vector ReadQueueValue(NT_Handle subentry); /** @} */ /** - * @defgroup ntcore_entrylistener_func Entry Listener Functions + * @defgroup ntcore_topic_func Topic Functions * @{ */ /** - * Entry listener callback function. - * Called when a key-value pair is changed. + * Get Published 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 + * 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 bitmask of NT_Type values; 0 is treated specially + * as a "don't care" + * @return Array of topic handles. */ -using EntryListenerCallback = - std::function value, unsigned int flags)>; +std::vector GetTopics(NT_Inst inst, std::string_view prefix, + unsigned int types); /** - * Add a listener for all entries starting with a certain prefix. + * Get Published Topics. * - * @param inst instance handle - * @param prefix UTF-8 string prefix - * @param callback listener to add - * @param flags NotifyKind bitmask - * @return Listener handle + * 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. */ -NT_EntryListener AddEntryListener( - NT_Inst inst, std::string_view prefix, - std::function callback, - unsigned int flags); +std::vector GetTopics(NT_Inst inst, std::string_view prefix, + wpi::span types); /** - * Add a listener for a single entry. + * Get Topic Information about multiple topics. * - * @param entry entry handle - * @param callback listener to add - * @param flags NotifyKind bitmask - * @return Listener handle + * 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. */ -NT_EntryListener AddEntryListener( - NT_Entry entry, - std::function callback, - unsigned int flags); +std::vector GetTopicInfo(NT_Inst inst, std::string_view prefix, + unsigned int types); /** - * Create a entry listener poller. + * 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 array of type strings + * @return Array of topic information. + */ +std::vector GetTopicInfo(NT_Inst inst, std::string_view prefix, + wpi::span 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 + */ +void 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, + wpi::span options = {}); + +/** + * 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, + wpi::span options = {}); + +/** + * 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, + wpi::span options = {}); + +/** + * 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, + wpi::span options = {}); + +/** + * 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_advancedsub_func Advanced Subscriber Functions + * @{ + */ + +/** + * 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 inst instance handle + * @param prefixes topic name prefixes + * @param options subscriber options + * @return subscriber handle + */ +NT_MultiSubscriber SubscribeMultiple( + NT_Inst inst, wpi::span prefixes, + wpi::span options = {}); + +/** + * Unsubscribes a multi-subscriber. + * + * @param sub multi-subscriber handle + */ +void UnsubscribeMultiple(NT_MultiSubscriber sub); + +/** @} */ + +/** + * @defgroup ntcore_topiclistener_func Topic Listener Functions + * @{ + */ + +/** + * Create a listener for changes to topics with names that start with any of + * the given prefixes. + * + * @param inst Instance handle + * @param prefixes Topic name string prefixes + * @param mask Bitmask of NT_TopicListenerFlags values + * @param callback Listener function + */ +NT_TopicListener AddTopicListener( + NT_Inst inst, wpi::span prefixes, unsigned int mask, + std::function callback); + +/** + * Create a listener for changes on a particular topic. + * + * @param handle Topic, subscriber, multi-subscriber, or entry handle + * @param mask Bitmask of NT_TopicListenerFlags values + * @param callback Listener function + */ +NT_TopicListener AddTopicListener( + NT_Handle handle, unsigned int mask, + std::function callback); + +/** + * Creates a topic 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 AddPolledTopicListener()) will be stored in the queue and + * must be collected by calling ReadTopicListenerQueue(). + * The returned handle must be destroyed with DestroyTopicListenerPoller(). * * @param inst instance handle * @return poller handle */ -NT_EntryListenerPoller CreateEntryListenerPoller(NT_Inst inst); +NT_TopicListenerPoller CreateTopicListenerPoller(NT_Inst inst); /** - * Destroy a entry listener poller. This will abort any blocked polling + * Destroys a topic 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 DestroyTopicListenerPoller(NT_TopicListenerPoller 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 topic 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 topic notifications. Returns empty array if no + * notifications since last call. */ -std::vector PollEntryListener(NT_EntryListenerPoller poller); +std::vector ReadTopicListenerQueue( + NT_TopicListenerPoller 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. + * Creates a polled topic listener. + * The caller is responsible for calling ReadTopicListenerQueue() to poll. * - * @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 poller poller handle + * @param prefixes array of UTF-8 string prefixes + * @param mask NT_NotifyKind bitmask + * @return Listener handle */ -std::vector PollEntryListener(NT_EntryListenerPoller poller, - double timeout, - bool* timed_out); +NT_TopicListener AddPolledTopicListener( + NT_TopicListenerPoller poller, wpi::span prefixes, + unsigned int mask); /** - * Cancel a PollEntryListener call. This wakes up a call to - * PollEntryListener for this poller and causes it to immediately return - * an empty array. + * Creates a polled topic listener. + * The caller is responsible for calling ReadTopicListenerQueue() to poll. * - * @param poller poller handle + * @param poller poller handle + * @param handle topic, subscriber, multi-subscriber, or entry handle + * @param mask NT_NotifyKind bitmask + * @return Listener handle */ -void CancelPollEntryListener(NT_EntryListenerPoller poller); +NT_TopicListener AddPolledTopicListener(NT_TopicListenerPoller poller, + NT_Handle handle, unsigned int mask); /** - * Remove an entry listener. + * Removes a topic listener. * - * @param entry_listener Listener handle to remove + * @param listener Listener handle to remove */ -void RemoveEntryListener(NT_EntryListener entry_listener); +void RemoveTopicListener(NT_TopicListener 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. + * @defgroup ntcore_valuelistener_func Value Listener Functions + * @{ + */ + +/** + * Create a listener for value changes on a subscriber. + * + * @param subentry Subscriber/entry + * @param mask Bitmask of NT_ValueListenerFlags values + * @param callback Listener function + */ +NT_ValueListener AddValueListener( + NT_Handle subentry, unsigned int mask, + std::function callback); + +/** + * Create a value listener poller. + * + * A poller provides a single queue of poll events. Events linked to this + * poller (using AddPolledValueListener()) will be stored in the queue and + * must be collected by calling ReadValueListenerQueue(). + * The returned handle must be destroyed with DestroyValueListenerPoller(). * * @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. + * @return poller handle */ -bool WaitForEntryListenerQueue(NT_Inst inst, double timeout); +NT_ValueListenerPoller CreateValueListenerPoller(NT_Inst inst); + +/** + * Destroy a value listener poller. This will abort any blocked polling + * call and prevent additional events from being generated for this poller. + * + * @param poller poller handle + */ +void DestroyValueListenerPoller(NT_ValueListenerPoller poller); + +/** + * Reads value listener queue (all value changes since last call). + * + * @param poller poller handle + * @return Array of value notifications. + */ +std::vector ReadValueListenerQueue( + NT_ValueListenerPoller poller); + +/** + * Create a polled value listener. + * The caller is responsible for calling ReadValueListenerQueue() to poll. + * + * @param poller poller handle + * @param subentry subscriber or entry handle + * @param mask NT_NotifyKind bitmask + * @return Listener handle + */ +NT_ValueListener AddPolledValueListener(NT_ValueListenerPoller poller, + NT_Handle subentry, unsigned int mask); + +/** + * Remove a value listener. + * + * @param listener Listener handle to remove + */ +void RemoveValueListener(NT_ValueListener listener); /** @} */ @@ -660,18 +936,6 @@ bool WaitForEntryListenerQueue(NT_Inst inst, double timeout); * @{ */ -/** - * Connection listener callback function. - * Called when a network connection is made or lost. - * - * @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 - */ -using ConnectionListenerCallback = - std::function; - /** * Add a connection listener. * @@ -726,34 +990,9 @@ NT_ConnectionListener AddPolledConnectionListener( * @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 PollConnectionListener( +std::vector ReadConnectionListenerQueue( 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 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. * @@ -761,216 +1000,6 @@ void CancelPollConnectionListener(NT_ConnectionListenerPoller poller); */ 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 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 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 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> 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> UnpackRpcValues( - std::string_view packed, wpi::span types); - /** @} */ /** @@ -1017,10 +1046,12 @@ void StopLocal(NT_Inst inst); * 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. @@ -1030,41 +1061,20 @@ void StartServer(NT_Inst inst, std::string_view persist_filename, void StopServer(NT_Inst inst); /** - * 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 inst instance handle */ -void StartClient(NT_Inst inst); +void StartClient3(NT_Inst inst); /** - * 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 inst instance handle - * @param server_name server name (UTF-8 string, null terminated) - * @param port port to communicate over + * @param inst instance handle */ -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. - * - * @param inst instance handle - * @param servers array of server name and port pairs - */ -void StartClient( - NT_Inst inst, - wpi::span> servers); - -/** - * 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 StartClientTeam(NT_Inst inst, unsigned int team, unsigned int port); +void StartClient4(NT_Inst inst); /** * Stops the client if it is running. @@ -1109,7 +1119,7 @@ void SetServerTeam(NT_Inst inst, unsigned int team, unsigned int port); * 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); @@ -1121,20 +1131,23 @@ void StartDSClient(NT_Inst inst, unsigned int port); 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 @@ -1163,64 +1176,6 @@ 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. - * - * @param inst instance handle - * @param filename filename - * @return error string, or nullptr if successful - */ -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 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 warn); - -/** @} */ - /** * @defgroup ntcore_utility_func Utility Functions * @{ @@ -1228,12 +1183,41 @@ const char* LoadEntries(NT_Inst inst, std::string_view filename, /** * 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); /** @} */ @@ -1345,29 +1329,7 @@ NT_Logger AddPolledLogger(NT_LoggerPoller poller, unsigned int min_level, * @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 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 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); +std::vector ReadLoggerQueue(NT_LoggerPoller poller); /** * Remove a logger. @@ -1376,28 +1338,7 @@ void CancelPollLogger(NT_LoggerPoller poller); */ 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); - /** @} */ /** @} */ -inline bool RpcAnswer::PostResponse(std::string_view result) const { - auto ret = PostRpcResponse(entry, call, result); - call = 0; - return ret; -} - } // 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 65d12430d0..d99ef4913d 100644 --- a/ntcore/src/main/native/include/ntcore_test.h +++ b/ntcore/src/main/native/include/ntcore_test.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_TEST_H_ -#define NTCORE_NTCORE_TEST_H_ +#pragma once #include @@ -82,5 +81,3 @@ struct NT_RpcCallInfo* NT_GetRpcCallInfoForTesting( const char* params, size_t params_len, int* struct_size); // No need for free as one already exists in the main library } // extern "C" - -#endif // NTCORE_NTCORE_TEST_H_ 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 3c38853112..59368998e8 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,12 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; 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.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; @@ -42,13 +41,19 @@ class ConnectionListenerTest { /** 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(); + 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,7 +62,6 @@ class ConnectionListenerTest { } @Test - @DisabledOnOs(OS.WINDOWS) void testJNI() { // set up the poller int poller = NetworkTablesJNI.createConnectionListenerPoller(m_serverInst.getHandle()); @@ -69,14 +73,13 @@ class ConnectionListenerTest { 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"); } + ConnectionNotification[] events = + NetworkTablesJNI.readConnectionListenerQueue(m_serverInst, poller); assertNotNull(events); assertEquals(1, events.length); @@ -92,13 +95,12 @@ class ConnectionListenerTest { } // 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.readConnectionListenerQueue(m_serverInst, poller); assertNotNull(events); assertEquals(1, events.length); @@ -109,33 +111,51 @@ class ConnectionListenerTest { 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); + m_serverInst.startServer("connectionlistenertest.json", address, 0, threadedPort); List events = new ArrayList<>(); - final int handle = m_serverInst.addConnectionListener(events::add, false); + final int handle = + m_serverInst.addConnectionListener( + e -> { + synchronized (events) { + events.add(e); + } + }, + false); // trigger a connect event - m_clientInst.startClient(address, threadedPort); + m_clientInst.startClient4(); + 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"); + } // 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); + assertTrue(events.get(0).connected); + events.clear(); + } // trigger a disconnect event m_clientInst.stopClient(); @@ -146,9 +166,15 @@ class ConnectionListenerTest { } // 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); + assertFalse(events.get(0).connected); + } } } 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 1dc26abe42..afc0c0ec44 100644 --- a/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java +++ b/ntcore/src/test/java/edu/wpi/first/networktables/LoggerTest.java @@ -31,12 +31,18 @@ class LoggerTest { List msgs = new ArrayList<>(); m_clientInst.addLogger(msgs::add, LogMessage.kInfo, 100); - m_clientInst.startClient("127.0.0.1", 10000); + m_clientInst.startClient4(); + 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() & NetworkTableInstance.kNetModeClient4) == 0) { 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/EntryListenerTest.java b/ntcore/src/test/java/edu/wpi/first/networktables/TopicListenerTest.java similarity index 59% rename from ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java rename to ntcore/src/test/java/edu/wpi/first/networktables/TopicListenerTest.java index 7b31c263ec..6b94d70563 100644 --- a/ntcore/src/test/java/edu/wpi/first/networktables/EntryListenerTest.java +++ b/ntcore/src/test/java/edu/wpi/first/networktables/TopicListenerTest.java @@ -4,18 +4,16 @@ 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 edu.wpi.first.util.WPIUtilJNI; 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 EntryListenerTest { +class TopicListenerTest { private NetworkTableInstance m_serverInst; private NetworkTableInstance m_clientInst; @@ -35,14 +33,15 @@ class EntryListenerTest { } private void connect() { - m_serverInst.startServer("connectionlistenertest.ini", "127.0.0.1", 10010); - m_clientInst.startClient("127.0.0.1", 10010); + m_serverInst.startServer("topiclistenertest.json", "127.0.0.1", 0, 10010); + m_clientInst.startClient4(); + m_clientInst.setServer("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) { + if (WPIUtilJNI.waitForObjectTimeout(poller, 1.0)) { fail("client didn't connect to server"); } } catch (InterruptedException ex) { @@ -52,11 +51,14 @@ class EntryListenerTest { } /** Test prefix with a new remote. */ + @Disabled("unreliable in CI") @Test void testPrefixNewRemote() { connect(); - List events = new ArrayList<>(); - final int handle = m_serverInst.addEntryListener("/foo", events::add, EntryListenerFlags.kNew); + final int poller = NetworkTablesJNI.createTopicListenerPoller(m_serverInst.getHandle()); + final int handle = + NetworkTablesJNI.addPolledTopicListener( + poller, new String[] {"/foo"}, TopicListenerFlags.kPublish); // Trigger an event m_clientInst.getEntry("/foo/bar").setDouble(1.0); @@ -68,16 +70,21 @@ class EntryListenerTest { fail("interrupted while waiting for entries to update"); } - assertTrue(m_serverInst.waitForEntryListenerQueue(1.0)); + try { + if (WPIUtilJNI.waitForObjectTimeout(poller, 1.0)) { + fail("never got signaled"); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + fail("interrupted while waiting for signal"); + } + TopicNotification[] events = NetworkTablesJNI.readTopicListenerQueue(m_serverInst, poller); // 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)); + assertEquals(1, events.length); + assertEquals(handle, events[0].listener); + assertEquals(m_serverInst.getTopic("/foo/bar"), events[0].info.getTopic()); + assertEquals("/foo/bar", events[0].info.name); + assertEquals(TopicListenerFlags.kPublish, events[0].flags); } } diff --git a/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp b/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp index 14c327c9d8..9c22216578 100644 --- a/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp +++ b/ntcore/src/test/native/cpp/ConnectionListenerTest.cpp @@ -5,6 +5,9 @@ #include #include +#include +#include + #include "TestPrinters.h" #include "gtest/gtest.h" #include "ntcore_cpp.h" @@ -22,20 +25,27 @@ class ConnectionListenerTest : public ::testing::Test { 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); + 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)); } @@ -49,13 +59,13 @@ TEST_F(ConnectionListenerTest, Polled) { 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::ReadConnectionListenerQueue(poller); ASSERT_EQ(result.size(), 1u); EXPECT_EQ(handle, result[0].listener); EXPECT_TRUE(result[0].connected); @@ -65,41 +75,61 @@ TEST_F(ConnectionListenerTest, Polled) { 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::ReadConnectionListenerQueue(poller); ASSERT_EQ(result.size(), 1u); EXPECT_EQ(handle, result[0].listener); EXPECT_FALSE(result[0].connected); - - // trigger a disconnect event } -TEST_F(ConnectionListenerTest, Threaded) { +class ConnectionListenerVariantTest + : public ConnectionListenerTest, + public ::testing::WithParamInterface> {}; + +TEST_P(ConnectionListenerVariantTest, Threaded) { + wpi::mutex m; std::vector result; auto handle = nt::AddConnectionListener( server_inst, - [&](const nt::ConnectionNotification& event) { result.push_back(event); }, + [&](const nt::ConnectionNotification& event) { + std::scoped_lock lock{m}; + result.push_back(event); + }, false); // 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].connected); + result.clear(); + } // trigger a disconnect event nt::StopClient(client_inst); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 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_FALSE(result[0].connected); + } } + +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 83494846c7..0000000000 --- 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 -#include - -#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 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 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 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 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 e781b49b8c..0000000000 --- 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 -#include - -#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(g1)) { - ++g1count; - EXPECT_TRUE((result.flags & NT_NOTIFY_NEW) != 0); - } else if (Handle{result.listener}.GetIndex() == static_cast(g2)) { - ++g2count; - EXPECT_TRUE((result.flags & NT_NOTIFY_DELETE) != 0); - } else if (Handle{result.listener}.GetIndex() == static_cast(g3)) { - ++g3count; - EXPECT_TRUE((result.flags & NT_NOTIFY_UPDATE) != 0); - } else if (Handle{result.listener}.GetIndex() == static_cast(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(g1)) { - ++g1count; - EXPECT_TRUE((result.flags & NT_NOTIFY_NEW) != 0); - } else if (Handle{result.listener}.GetIndex() == static_cast(g2)) { - ++g2count; - EXPECT_TRUE((result.flags & NT_NOTIFY_DELETE) != 0); - } else if (Handle{result.listener}.GetIndex() == static_cast(g3)) { - ++g3count; - EXPECT_TRUE((result.flags & NT_NOTIFY_UPDATE) != 0); - } else if (Handle{result.listener}.GetIndex() == static_cast(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 0000000000..638250dcfb --- /dev/null +++ b/ntcore/src/test/native/cpp/LocalStorageTest.cpp @@ -0,0 +1,469 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 "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" + +using ::testing::_; +using ::testing::Return; + +namespace nt { + +::testing::Matcher IsPubSubOptions( + const PubSubOptions& good) { + return ::testing::AllOf( + ::testing::Field("periodic", &PubSubOptions::periodic, good.periodic), + ::testing::Field("pollStorageSize", &PubSubOptions::pollStorageSize, + good.pollStorageSize), + ::testing::Field("logging", &PubSubOptions::sendAll, good.sendAll), + ::testing::Field("keepDuplicates", &PubSubOptions::keepDuplicates, + good.keepDuplicates)); +} + +class LocalStorageTest : public ::testing::Test { + public: + LocalStorageTest() { + storage.StartNetwork(startup); + storage.SetNetwork(&network); + } + + ::testing::StrictMock startup; + ::testing::StrictMock network; + wpi::MockLogger logger; + LocalStorage storage{0, 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, 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(), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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, + IsPubSubOptions({}))); + 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"}}), + IsPubSubOptions({}))); + auto sub = storage.Subscribe(fooTopic, NT_UNASSIGNED, "", {}); + + EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"}, + std::string_view{"boolean"}, wpi::json::object(), + IsPubSubOptions({}))); + 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(true, 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(), + IsPubSubOptions({}))); + 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"}}), + IsPubSubOptions({}))); + 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"}}), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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(true, 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"}}), + IsPubSubOptions({}))); + auto sub = storage.Subscribe(fooTopic, NT_INTEGER, "int", {}); + + EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"}, + std::string_view{"boolean"}, wpi::json::object(), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {}); + + EXPECT_CALL(network, Subscribe(_, wpi::SpanEq({std::string{"foo"}}), + IsPubSubOptions({}))); + EXPECT_CALL(logger, Call(NT_LOG_INFO, _, _, + "local subscribe to 'foo' disabled due to type " + "mismatch (wanted 'int', currently '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(), + IsPubSubOptions({}))); + + 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(), + IsPubSubOptions({}))); + + 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"}}), + IsPubSubOptions({}))); + 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"}}), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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(_, _, IsPubSubOptions({}))); + 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"}}), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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"}}), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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(), + IsPubSubOptions({}))); + 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"}}), + IsPubSubOptions({}))); + auto entry = storage.GetEntry(fooTopic, NT_UNASSIGNED, "", {}); + EXPECT_FALSE(storage.SetEntryValue(entry, {})); +} + +TEST_F(LocalStorageTest, PublishUntyped) { + EXPECT_EQ(storage.Publish(fooTopic, NT_UNASSIGNED, "", {}, {}), 0u); +} + +TEST_F(LocalStorageTest, SetValueInvalidHandle) { + EXPECT_FALSE(storage.SetEntryValue(0u, {})); +} + +} // 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 7afeeef9f9..0000000000 --- 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 -#include -#include - -#include "Message.h" -#include "TestPrinters.h" -#include "gmock/gmock.h" - -namespace nt { - -class MessageMatcher - : public ::testing::MatcherInterface> { - public: - explicit MessageMatcher(std::shared_ptr goodmsg_) - : goodmsg(std::move(goodmsg_)) {} - - bool MatchAndExplain(std::shared_ptr msg, - ::testing::MatchResultListener* listener) const override; - void DescribeTo(::std::ostream* os) const override; - void DescribeNegationTo(::std::ostream* os) const override; - - private: - std::shared_ptr goodmsg; -}; - -inline ::testing::Matcher> MessageEq( - std::shared_ptr 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 0000000000..cd93343c86 --- /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 + +#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, 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 d632d5c57a..0000000000 --- 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 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 22b0fba9d0..0000000000 --- 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 - -#include "IDispatcher.h" -#include "gmock/gmock.h" - -namespace nt { - -class MockDispatcher : public IDispatcher { - public: - MOCK_METHOD3(QueueOutgoing, - void(std::shared_ptr 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 58518c687e..0000000000 --- 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 - -#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 callback, - std::string_view prefix, unsigned int flags)); - MOCK_METHOD3( - Add, - unsigned int(std::function 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, unsigned int flags, - unsigned int only_listener)); -}; - -} // namespace nt - -#endif // NTCORE_MOCKENTRYNOTIFIER_H_ diff --git a/ntcore/src/test/native/cpp/MockLogger.h b/ntcore/src/test/native/cpp/MockLogger.h new file mode 100644 index 0000000000..f20a12f608 --- /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 + +#include "gmock/gmock.h" + +namespace wpi { + +class MockLogger : public Logger, + public ::testing::MockFunction { + 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 be9c2c67b6..0000000000 --- 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 - -#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 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 be9e5129cf..0000000000 --- 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/PubSubOptionsMatcher.cpp b/ntcore/src/test/native/cpp/PubSubOptionsMatcher.cpp new file mode 100644 index 0000000000..5c3d067c47 --- /dev/null +++ b/ntcore/src/test/native/cpp/PubSubOptionsMatcher.cpp @@ -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. + +#include "PubSubOptionsMatcher.h" + +#include "TestPrinters.h" + +namespace nt { + +bool PubSubOptionsMatcher::MatchAndExplain( + const PubSubOptions& val, ::testing::MatchResultListener* listener) const { + bool match = true; + if (val.periodic != good.periodic) { + *listener << "periodic mismatch "; + match = false; + } + if (val.pollStorageSize != good.pollStorageSize) { + *listener << "pollStorageSize mismatch "; + match = false; + } + if (val.sendAll != good.sendAll) { + *listener << "logging 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 0000000000..219c7e13e0 --- /dev/null +++ b/ntcore/src/test/native/cpp/PubSubOptionsMatcher.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 +#include + +#include "PubSubOptions.h" +#include "gmock/gmock.h" + +namespace nt { + +class PubSubOptionsMatcher + : public ::testing::MatcherInterface { + public: + explicit PubSubOptionsMatcher(PubSubOptions good) : good{std::move(good)} {} + + bool MatchAndExplain(const PubSubOptions& val, + ::testing::MatchResultListener* listener) const override; + void DescribeTo(::std::ostream* os) const override; + void DescribeNegationTo(::std::ostream* os) const override; + + private: + PubSubOptions good; +}; + +inline ::testing::Matcher PubSubOptionsEq( + PubSubOptions 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 0000000000..28814b3ead --- /dev/null +++ b/ntcore/src/test/native/cpp/SpanMatcher.h @@ -0,0 +1,73 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "TestPrinters.h" +#include "gmock/gmock.h" + +namespace wpi { + +template +class SpanMatcher : public ::testing::MatcherInterface> { + public: + explicit SpanMatcher(span good_) : good{good_.begin(), good_.end()} {} + + bool MatchAndExplain(span val, + ::testing::MatchResultListener* listener) const override; + void DescribeTo(::std::ostream* os) const override; + void DescribeNegationTo(::std::ostream* os) const override; + + private: + std::vector> good; +}; + +template +inline ::testing::Matcher> SpanEq(span good) { + return ::testing::MakeMatcher(new SpanMatcher(good)); +} + +template +inline ::testing::Matcher> SpanEq( + std::initializer_list good) { + return ::testing::MakeMatcher( + new SpanMatcher({good.begin(), good.end()})); +} + +template +bool SpanMatcher::MatchAndExplain( + span val, ::testing::MatchResultListener* listener) const { + if (val.size() != good.size() || + !std::equal(val.begin(), val.end(), good.begin())) { + return false; + } + return true; +} + +template +void SpanMatcher::DescribeTo(::std::ostream* os) const { + PrintTo(span{good}, os); +} + +template +void SpanMatcher::DescribeNegationTo(::std::ostream* os) const { + *os << "is not equal to "; + PrintTo(span{good}, os); +} + +} // namespace wpi + +inline wpi::span operator"" _us(const char* str, size_t len) { + return {reinterpret_cast(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 e1ee1c7fab..0000000000 --- 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 -#include -#include -#include - -#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 { - 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{})); - storage.SetEntryTypeValue("booleanarr/one", - Value::MakeBooleanArray(std::vector{1})); - storage.SetEntryTypeValue("booleanarr/two", - Value::MakeBooleanArray(std::vector{1, 0})); - storage.SetEntryTypeValue("doublearr/empty", - Value::MakeDoubleArray(std::vector{})); - storage.SetEntryTypeValue("doublearr/one", - Value::MakeDoubleArray(std::vector{0.5})); - storage.SetEntryTypeValue( - "doublearr/two", - Value::MakeDoubleArray(std::vector{0.5, -0.25})); - storage.SetEntryTypeValue( - "stringarr/empty", Value::MakeStringArray(std::vector{})); - storage.SetEntryTypeValue( - "stringarr/one", - Value::MakeStringArray(std::vector{"hello"})); - storage.SetEntryTypeValue( - "stringarr/two", - Value::MakeStringArray(std::vector{"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{}), - *storage.GetEntryValue("booleanarr/empty")); - EXPECT_EQ(*Value::MakeBooleanArray(std::vector{1}), - *storage.GetEntryValue("booleanarr/one")); - EXPECT_EQ(*Value::MakeBooleanArray(std::vector{1, 0}), - *storage.GetEntryValue("booleanarr/two")); - EXPECT_EQ(*Value::MakeDoubleArray(std::vector{}), - *storage.GetEntryValue("doublearr/empty")); - EXPECT_EQ(*Value::MakeDoubleArray(std::vector{0.5}), - *storage.GetEntryValue("doublearr/one")); - EXPECT_EQ(*Value::MakeDoubleArray(std::vector{0.5, -0.25}), - *storage.GetEntryValue("doublearr/two")); - EXPECT_EQ(*Value::MakeStringArray(std::vector{}), - *storage.GetEntryValue("stringarr/empty")); - EXPECT_EQ(*Value::MakeStringArray(std::vector{"hello"}), - *storage.GetEntryValue("stringarr/one")); - EXPECT_EQ( - *Value::MakeStringArray(std::vector{"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(); - 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(); - 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(); - 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(); - 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 3a1755f048..a0cc737bff 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 #include @@ -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 @@ class StorageTest { void HookOutgoing(bool server) { storage.SetDispatcher(&dispatcher, server); } wpi::Logger logger; - ::testing::StrictMock notifier; - ::testing::StrictMock rpc_server; ::testing::StrictMock dispatcher; Storage storage; Storage::Entry tmp_entry; }; } // namespace nt - -#endif // NTCORE_STORAGETEST_H_ diff --git a/ntcore/src/test/native/cpp/TestPrinters.cpp b/ntcore/src/test/native/cpp/TestPrinters.cpp index 49b407ba24..8213468ff8 100644 --- a/ntcore/src/test/native/cpp/TestPrinters.cpp +++ b/ntcore/src/test/native/cpp/TestPrinters.cpp @@ -4,13 +4,23 @@ #include "TestPrinters.h" +#include + #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 nt { +namespace wpi { +void PrintTo(const json& val, ::std::ostream* os) { + *os << val.dump(); +} +} // namespace wpi +namespace nt { +#if 0 void PrintTo(const EntryNotification& event, std::ostream* os) { *os << "EntryNotification{listener="; PrintTo(Handle{event.listener}, os); @@ -20,7 +30,7 @@ void PrintTo(const EntryNotification& event, std::ostream* os) { PrintTo(event.value, os); *os << '}'; } - +#endif void PrintTo(const Handle& handle, std::ostream* os) { *os << "Handle{"; switch (handle.GetType()) { @@ -48,11 +58,14 @@ void PrintTo(const Handle& handle, std::ostream* os) { case Handle::kLoggerPoller: *os << "kLoggerPoller"; break; - case Handle::kRpcCall: - *os << "kRpcCall"; + case Handle::kTopic: + *os << "kTopic"; break; - case Handle::kRpcCallPoller: - *os << "kRpcCallPoller"; + case Handle::kSubscriber: + *os << "kSubscriber"; + break; + case Handle::kPublisher: + *os << "kPublisher"; break; default: *os << "UNKNOWN"; @@ -61,46 +74,46 @@ void PrintTo(const Handle& handle, std::ostream* os) { *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: @@ -125,6 +138,12 @@ void PrintTo(const Value& value, std::ostream* os) { case NT_DOUBLE: *os << value.GetDouble(); break; + case NT_FLOAT: + *os << value.GetFloat(); + break; + case NT_INTEGER: + *os << value.GetInteger(); + break; case NT_STRING: *os << '"' << value.GetString() << '"'; break; @@ -137,12 +156,15 @@ void PrintTo(const Value& value, std::ostream* os) { case NT_DOUBLE_ARRAY: *os << ::testing::PrintToString(value.GetDoubleArray()); break; + case NT_FLOAT_ARRAY: + *os << ::testing::PrintToString(value.GetFloatArray()); + break; + case NT_INTEGER_ARRAY: + *os << ::testing::PrintToString(value.GetIntegerArray()); + break; case NT_STRING_ARRAY: *os << ::testing::PrintToString(value.GetStringArray()); break; - case NT_RPC: - *os << ::testing::PrintToString(value.GetRpc()); - break; default: *os << "UNKNOWN TYPE " << value.type(); break; @@ -150,4 +172,11 @@ void PrintTo(const Value& value, std::ostream* os) { *os << '}'; } +void PrintTo(const PubSubOptions& options, std::ostream* os) { + *os << "PubSubOptions{periodic=" << options.periodic + << ", pollStorageSize=" << options.pollStorageSize + << ", logging=" << 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 47c9eb4955..381f56160b 100644 --- a/ntcore/src/test/native/cpp/TestPrinters.h +++ b/ntcore/src/test/native/cpp/TestPrinters.h @@ -2,54 +2,65 @@ // Open Source 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 #include #include #include +#include + #include "gtest/gtest.h" namespace wpi { +class json; + inline void PrintTo(std::string_view str, ::std::ostream* os) { ::testing::internal::PrintStringTo(std::string{str}, os); } +template +void PrintTo(span 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 EntryNotification; class Handle; -class Message; +class PubSubOptions; class Value; -void PrintTo(const EntryNotification& event, std::ostream* os); +// void PrintTo(const EntryNotification& 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 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, std::ostream* os) { - *os << "shared_ptr{"; - if (value) { - PrintTo(*value, os); - } - *os << '}'; -} +void PrintTo(const PubSubOptions& options, std::ostream* os); } // namespace nt - -#endif // NTCORE_TESTPRINTERS_H_ diff --git a/ntcore/src/test/native/cpp/TopicListenerTest.cpp b/ntcore/src/test/native/cpp/TopicListenerTest.cpp new file mode 100644 index 0000000000..e7ff5a742c --- /dev/null +++ b/ntcore/src/test/native/cpp/TopicListenerTest.cpp @@ -0,0 +1,286 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include +#include + +#include +#include + +#include "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()) { + nt::SetNetworkIdentity(m_serverInst, "server"); + nt::SetNetworkIdentity(m_clientInst, "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 + } + + ~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& events, + NT_TopicListener 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); + nt::SetServer(m_clientInst, "127.0.0.1", port); + + // Use connection listener to ensure we've connected + NT_ConnectionListenerPoller poller = + nt::CreateConnectionListenerPoller(m_clientInst); + nt::AddPolledConnectionListener(poller, false); + 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& events, NT_TopicListener handle, + unsigned int flags, std::string_view topicName) { + ASSERT_EQ(events.size(), 1u); + ASSERT_EQ(events[0].listener, handle); + ASSERT_EQ(events[0].info.topic, nt::GetTopic(m_serverInst, topicName)); + ASSERT_EQ(events[0].info.name, topicName); + ASSERT_EQ(events[0].flags, flags); +} + +TEST_F(TopicListenerTest, TopicNewLocal) { + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = nt::AddPolledTopicListener( + poller, nt::GetTopic(m_serverInst, "/foo"), NT_TOPIC_NOTIFY_PUBLISH); + + PublishTopics(m_serverInst); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + auto events = nt::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_PUBLISH, "/foo"); +} + +TEST_F(TopicListenerTest, DISABLED_TopicNewRemote) { + Connect(10010); + if (HasFatalFailure()) { + return; + } + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = nt::AddPolledTopicListener( + poller, nt::GetTopic(m_serverInst, "/foo"), NT_TOPIC_NOTIFY_PUBLISH); + + 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::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_PUBLISH, "/foo"); +} + +TEST_F(TopicListenerTest, TopicPublishImm) { + PublishTopics(m_serverInst); + + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = nt::AddPolledTopicListener( + poller, nt::GetTopic(m_serverInst, "/foo"), + NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + auto events = nt::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, + NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE, "/foo"); +} + +TEST_F(TopicListenerTest, TopicUnpublishPropsImm) { + PublishTopics(m_serverInst); + + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + nt::AddPolledTopicListener(poller, nt::GetTopic(m_serverInst, "/foo"), + NT_TOPIC_NOTIFY_UNPUBLISH | + NT_TOPIC_NOTIFY_PROPERTIES | + NT_TOPIC_NOTIFY_IMMEDIATE); + + bool timedOut = false; + ASSERT_FALSE(wpi::WaitForObject(poller, 0.02, &timedOut)); + auto events = nt::ReadTopicListenerQueue(poller); + ASSERT_TRUE(events.empty()); +} + +TEST_F(TopicListenerTest, TopicUnpublishLocal) { + auto topic = nt::GetTopic(m_serverInst, "/foo"); + + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = + nt::AddPolledTopicListener(poller, topic, NT_TOPIC_NOTIFY_UNPUBLISH); + + 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::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_UNPUBLISH, "/foo"); +} + +TEST_F(TopicListenerTest, DISABLED_TopicUnpublishRemote) { + Connect(10010); + if (HasFatalFailure()) { + return; + } + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = nt::AddPolledTopicListener( + poller, nt::GetTopic(m_serverInst, "/foo"), NT_TOPIC_NOTIFY_UNPUBLISH); + + 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::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_UNPUBLISH, "/foo"); +} + +TEST_F(TopicListenerTest, TopicPropertiesLocal) { + auto topic = nt::GetTopic(m_serverInst, "/foo"); + + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = + nt::AddPolledTopicListener(poller, topic, NT_TOPIC_NOTIFY_PROPERTIES); + + nt::SetTopicProperty(topic, "foo", 5); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + auto events = nt::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_PROPERTIES, "/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::CreateTopicListenerPoller(m_serverInst); + auto handle = nt::AddPolledTopicListener( + poller, nt::GetTopic(m_serverInst, "/foo"), NT_TOPIC_NOTIFY_PROPERTIES); + 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::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_PROPERTIES, "/foo"); +} + +TEST_F(TopicListenerTest, PrefixPublishLocal) { + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = + nt::AddPolledTopicListener(poller, {{"/foo/"}}, NT_TOPIC_NOTIFY_PUBLISH); + + PublishTopics(m_serverInst); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + auto events = nt::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_PUBLISH); +} + +TEST_F(TopicListenerTest, DISABLED_PrefixPublishRemote) { + Connect(10011); + if (HasFatalFailure()) { + return; + } + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = + nt::AddPolledTopicListener(poller, {{"/foo/"}}, NT_TOPIC_NOTIFY_PUBLISH); + + 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::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, NT_TOPIC_NOTIFY_PUBLISH); +} + +TEST_F(TopicListenerTest, PrefixPublishImm) { + PublishTopics(m_serverInst); + + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + auto handle = nt::AddPolledTopicListener( + poller, {{"/foo/"}}, NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + auto events = nt::ReadTopicListenerQueue(poller); + CheckEvents(events, handle, + NT_TOPIC_NOTIFY_PUBLISH | NT_TOPIC_NOTIFY_IMMEDIATE); +} + +TEST_F(TopicListenerTest, PrefixUnpublishPropsImm) { + PublishTopics(m_serverInst); + + auto poller = nt::CreateTopicListenerPoller(m_serverInst); + nt::AddPolledTopicListener(poller, {{"/foo/"}}, + NT_TOPIC_NOTIFY_UNPUBLISH | + NT_TOPIC_NOTIFY_PROPERTIES | + NT_TOPIC_NOTIFY_IMMEDIATE); + + bool timedOut = false; + ASSERT_FALSE(wpi::WaitForObject(poller, 0.02, &timedOut)); + auto events = nt::ReadTopicListenerQueue(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 0000000000..ba3c3e99e5 --- /dev/null +++ b/ntcore/src/test/native/cpp/ValueListenerTest.cpp @@ -0,0 +1,280 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include +#include + +#include "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()} { + nt::SetNetworkIdentity(m_inst, "server"); + } + + ~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::CreateValueListenerPoller(m_inst); + auto poller2 = nt::CreateValueListenerPoller(m_inst); + auto poller3 = nt::CreateValueListenerPoller(m_inst); + auto h1 = nt::AddPolledValueListener(poller1, sub, NT_VALUE_NOTIFY_LOCAL); + auto h2 = nt::AddPolledValueListener(poller2, sub, NT_VALUE_NOTIFY_LOCAL); + auto h3 = nt::AddPolledValueListener(poller3, sub, NT_VALUE_NOTIFY_LOCAL); + + 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::ReadValueListenerQueue(poller1); + auto results2 = nt::ReadValueListenerQueue(poller2); + auto results3 = nt::ReadValueListenerQueue(poller3); + + ASSERT_EQ(results1.size(), 1u); + EXPECT_EQ(results1[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results1[0].listener, h1); + EXPECT_EQ(results1[0].subentry, sub); + EXPECT_EQ(results1[0].topic, topic); + EXPECT_EQ(results1[0].value, nt::Value::MakeDouble(0.0)); + + ASSERT_EQ(results2.size(), 1u); + EXPECT_EQ(results2[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results2[0].listener, h2); + EXPECT_EQ(results2[0].subentry, sub); + EXPECT_EQ(results2[0].topic, topic); + EXPECT_EQ(results2[0].value, nt::Value::MakeDouble(0.0)); + + ASSERT_EQ(results3.size(), 1u); + EXPECT_EQ(results3[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results3[0].listener, h3); + EXPECT_EQ(results3[0].subentry, sub); + EXPECT_EQ(results3[0].topic, topic); + EXPECT_EQ(results3[0].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::CreateValueListenerPoller(m_inst); + auto h1 = nt::AddPolledValueListener(poller, sub1, NT_VALUE_NOTIFY_LOCAL); + auto h2 = nt::AddPolledValueListener(poller, sub2, NT_VALUE_NOTIFY_LOCAL); + + nt::SetDouble(pub, 0); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + ASSERT_FALSE(timedOut); + auto results = nt::ReadValueListenerQueue(poller); + + ASSERT_EQ(results.size(), 2u); + EXPECT_EQ(results[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[0].listener, h1); + EXPECT_EQ(results[0].subentry, sub1); + EXPECT_EQ(results[0].topic, topic); + EXPECT_EQ(results[0].value, nt::Value::MakeDouble(0.0)); + + EXPECT_EQ(results[1].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[1].listener, h2); + EXPECT_EQ(results[1].subentry, sub2); + EXPECT_EQ(results[1].topic, topic); + EXPECT_EQ(results[1].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::CreateValueListenerPoller(m_inst); + auto h1 = nt::AddPolledValueListener(poller, sub1, NT_VALUE_NOTIFY_LOCAL); + auto h2 = nt::AddPolledValueListener(poller, sub2, NT_VALUE_NOTIFY_LOCAL); + + 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::ReadValueListenerQueue(poller); + + ASSERT_EQ(results.size(), 2u); + EXPECT_EQ(results[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[0].listener, h1); + EXPECT_EQ(results[0].subentry, sub1); + EXPECT_EQ(results[0].topic, topic1); + EXPECT_EQ(results[0].value, nt::Value::MakeDouble(0.0)); + + EXPECT_EQ(results[1].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[1].listener, h2); + EXPECT_EQ(results[1].subentry, sub2); + EXPECT_EQ(results[1].topic, topic2); + EXPECT_EQ(results[1].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::CreateValueListenerPoller(m_inst); + auto h = nt::AddPolledValueListener(poller, sub, NT_VALUE_NOTIFY_LOCAL); + + 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::ReadValueListenerQueue(poller); + + ASSERT_EQ(results.size(), 2u); + EXPECT_EQ(results[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[0].listener, h); + EXPECT_EQ(results[0].subentry, sub); + EXPECT_EQ(results[0].topic, topic1); + EXPECT_EQ(results[0].value, nt::Value::MakeDouble(0.0)); + + EXPECT_EQ(results[1].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[1].listener, h); + EXPECT_EQ(results[1].subentry, sub); + EXPECT_EQ(results[1].topic, topic2); + EXPECT_EQ(results[1].value, nt::Value::MakeDouble(1.0)); +} + +TEST_F(ValueListenerTest, PollEntry) { + auto entry = nt::GetEntry(m_inst, "foo"); + + auto poller = nt::CreateValueListenerPoller(m_inst); + auto h = nt::AddPolledValueListener(poller, entry, NT_VALUE_NOTIFY_LOCAL); + + ASSERT_TRUE(nt::SetDouble(entry, 0)); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + ASSERT_FALSE(timedOut); + auto results = nt::ReadValueListenerQueue(poller); + + ASSERT_EQ(results.size(), 1u); + EXPECT_EQ(results[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[0].listener, h); + EXPECT_EQ(results[0].subentry, entry); + EXPECT_EQ(results[0].topic, nt::GetTopic(m_inst, "foo")); + EXPECT_EQ(results[0].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::CreateValueListenerPoller(m_inst); + auto h = nt::AddPolledValueListener( + poller, entry, NT_VALUE_NOTIFY_LOCAL | NT_VALUE_NOTIFY_IMMEDIATE); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + ASSERT_FALSE(timedOut); + auto results = nt::ReadValueListenerQueue(poller); + + ASSERT_EQ(results.size(), 1u); + EXPECT_EQ(results[0].flags, NT_VALUE_NOTIFY_IMMEDIATE); + EXPECT_EQ(results[0].listener, h); + EXPECT_EQ(results[0].subentry, entry); + EXPECT_EQ(results[0].topic, nt::GetTopic(m_inst, "foo")); + EXPECT_EQ(results[0].value, nt::Value::MakeDouble(0.0)); +} + +TEST_F(ValueListenerTest, PollImmediateNoValue) { + auto entry = nt::GetEntry(m_inst, "foo"); + + auto poller = nt::CreateValueListenerPoller(m_inst); + auto h = nt::AddPolledValueListener( + poller, entry, NT_VALUE_NOTIFY_LOCAL | NT_VALUE_NOTIFY_IMMEDIATE); + + bool timedOut = false; + ASSERT_FALSE(wpi::WaitForObject(poller, 0.02, &timedOut)); + ASSERT_TRUE(timedOut); + auto results = nt::ReadValueListenerQueue(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::ReadValueListenerQueue(poller); + ASSERT_FALSE(timedOut); + + ASSERT_EQ(results.size(), 1u); + EXPECT_EQ(results[0].flags, NT_VALUE_NOTIFY_LOCAL); + EXPECT_EQ(results[0].listener, h); + EXPECT_EQ(results[0].subentry, entry); + EXPECT_EQ(results[0].topic, nt::GetTopic(m_inst, "foo")); + EXPECT_EQ(results[0].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::CreateValueListenerPoller(m_inst); + auto h = nt::AddPolledValueListener( + poller, sub, NT_VALUE_NOTIFY_LOCAL | NT_VALUE_NOTIFY_IMMEDIATE); + + bool timedOut = false; + ASSERT_TRUE(wpi::WaitForObject(poller, 1.0, &timedOut)); + ASSERT_FALSE(timedOut); + auto results = nt::ReadValueListenerQueue(poller); + + ASSERT_EQ(results.size(), 2u); + EXPECT_EQ(results[0].flags, NT_VALUE_NOTIFY_IMMEDIATE); + EXPECT_EQ(results[0].listener, h); + EXPECT_EQ(results[0].subentry, sub); + EXPECT_EQ(results[0].topic, topic1); + EXPECT_EQ(results[0].value, nt::Value::MakeDouble(0.0)); + + EXPECT_EQ(results[1].flags, NT_VALUE_NOTIFY_IMMEDIATE); + EXPECT_EQ(results[1].listener, h); + EXPECT_EQ(results[1].subentry, sub); + EXPECT_EQ(results[1].topic, topic2); + EXPECT_EQ(results[1].value, nt::Value::MakeDouble(1.0)); +} + +} // namespace nt diff --git a/ntcore/src/test/native/cpp/ValueMatcher.cpp b/ntcore/src/test/native/cpp/ValueMatcher.cpp index 1f55c0126f..7a0d453abc 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 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 ab68448d49..c77e04d158 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 #include @@ -14,26 +13,21 @@ namespace nt { -class ValueMatcher - : public ::testing::MatcherInterface> { +class ValueMatcher : public ::testing::MatcherInterface { public: - explicit ValueMatcher(std::shared_ptr goodval_) - : goodval(std::move(goodval_)) {} + explicit ValueMatcher(Value goodval_) : goodval(std::move(goodval_)) {} - bool MatchAndExplain(std::shared_ptr 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 goodval; + Value goodval; }; -inline ::testing::Matcher> ValueEq( - std::shared_ptr goodval) { +inline ::testing::Matcher 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 a9f221801c..2f73c3d538 100644 --- a/ntcore/src/test/native/cpp/ValueTest.cpp +++ b/ntcore/src/test/native/cpp/ValueTest.cpp @@ -13,8 +13,8 @@ using namespace std::string_view_literals; namespace wpi { -template -inline bool operator==(span lhs, span rhs) { +template +inline bool operator==(span lhs, span rhs) { if (lhs.size() != rhs.size()) { return false; } @@ -35,19 +35,19 @@ TEST_F(ValueTest, ConstructEmpty) { 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, Boolean) { 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, Double) { 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, String) { } TEST_F(ValueTest, Raw) { - auto v = Value::MakeRaw("hello"); - ASSERT_EQ(NT_RAW, v->type()); - ASSERT_EQ("hello", v->GetRaw()); + std::vector arr{5, 4, 3, 2, 1}; + auto v = Value::MakeRaw(arr); + ASSERT_EQ(NT_RAW, v.type()); + ASSERT_EQ(wpi::span(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(wpi::span(reinterpret_cast("\5\4\3\2\1"), 5), + wpi::span(cv.data.v_raw.data, 5)); - v = Value::MakeRaw("goodbye"); - ASSERT_EQ(NT_RAW, v->type()); - ASSERT_EQ("goodbye", v->GetRaw()); + std::vector arr2{1, 2, 3, 4, 5, 6}; + v = Value::MakeRaw(arr2); + ASSERT_EQ(NT_RAW, v.type()); + ASSERT_EQ(wpi::span(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(wpi::span(reinterpret_cast("\1\2\3\4\5\6"), 6), + wpi::span(cv.data.v_raw.data, 6)); NT_DisposeValue(&cv); } @@ -124,11 +128,11 @@ TEST_F(ValueTest, Raw) { TEST_F(ValueTest, BooleanArray) { std::vector vec{1, 0, 1}; auto v = Value::MakeBooleanArray(vec); - ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type()); - ASSERT_EQ(wpi::span(vec), v->GetBooleanArray()); + ASSERT_EQ(NT_BOOLEAN_ARRAY, v.type()); + ASSERT_EQ(wpi::span(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 @@ TEST_F(ValueTest, BooleanArray) { // assign with same size vec = {0, 1, 0}; v = Value::MakeBooleanArray(vec); - ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type()); - ASSERT_EQ(wpi::span(vec), v->GetBooleanArray()); + ASSERT_EQ(NT_BOOLEAN_ARRAY, v.type()); + ASSERT_EQ(wpi::span(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 @@ TEST_F(ValueTest, BooleanArray) { // assign with different size vec = {1, 0}; v = Value::MakeBooleanArray(vec); - ASSERT_EQ(NT_BOOLEAN_ARRAY, v->type()); - ASSERT_EQ(wpi::span(vec), v->GetBooleanArray()); + ASSERT_EQ(NT_BOOLEAN_ARRAY, v.type()); + ASSERT_EQ(wpi::span(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, BooleanArray) { TEST_F(ValueTest, DoubleArray) { std::vector vec{0.5, 0.25, 0.5}; auto v = Value::MakeDoubleArray(vec); - ASSERT_EQ(NT_DOUBLE_ARRAY, v->type()); - ASSERT_EQ(wpi::span(vec), v->GetDoubleArray()); + ASSERT_EQ(NT_DOUBLE_ARRAY, v.type()); + ASSERT_EQ(wpi::span(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 @@ TEST_F(ValueTest, DoubleArray) { // 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(vec), v->GetDoubleArray()); + ASSERT_EQ(NT_DOUBLE_ARRAY, v.type()); + ASSERT_EQ(wpi::span(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 @@ TEST_F(ValueTest, DoubleArray) { // assign with different size vec = {0.5, 0.25}; v = Value::MakeDoubleArray(vec); - ASSERT_EQ(NT_DOUBLE_ARRAY, v->type()); - ASSERT_EQ(wpi::span(vec), v->GetDoubleArray()); + ASSERT_EQ(NT_DOUBLE_ARRAY, v.type()); + ASSERT_EQ(wpi::span(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 @@ TEST_F(ValueTest, StringArray) { 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 @@ TEST_F(ValueTest, StringArray) { 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 @@ TEST_F(ValueTest, StringArray) { 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, UnassignedComparison) { 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 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 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 @@ TEST_F(ValueTest, StringArrayComparison) { 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 @@ TEST_F(ValueTest, StringArrayComparison) { 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 @@ TEST_F(ValueTest, StringArrayComparison) { 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 313fd989c7..0000000000 --- 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 - -#include -#include -#include -#include - -#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{0, 1, 0}); - v_boolean_array_big = Value::MakeBooleanArray(std::vector(255)); - v_double_array = Value::MakeDoubleArray(std::vector{0.5, 0.25}); - v_double_array_big = Value::MakeDoubleArray(std::vector(255)); - - std::vector 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 v_boolean, v_double, v_string, v_raw; - std::shared_ptr v_boolean_array, v_boolean_array_big; - std::shared_ptr v_double_array, v_double_array_big; - std::shared_ptr 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::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(val)); - EXPECT_EQ(*v_boolean, *val); - - auto v_false = Value::MakeBoolean(false); - val = d.ReadValue(NT_BOOLEAN); - ASSERT_TRUE(static_cast(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(val)); - EXPECT_EQ(*v_double, *val); - - val = d.ReadValue(NT_DOUBLE); - ASSERT_TRUE(static_cast(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(val)); - EXPECT_EQ(*v_string, *val); - - auto v_bye = Value::MakeString("bye"sv); - val = d.ReadValue(NT_STRING); - ASSERT_TRUE(static_cast(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(val)); - EXPECT_EQ(*v_boolean_array, *val); - - auto v_boolean_array2 = Value::MakeBooleanArray(std::vector{1, 0}); - val = d.ReadValue(NT_BOOLEAN_ARRAY); - ASSERT_TRUE(static_cast(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(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(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(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(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(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(val)); - EXPECT_EQ(*v_boolean, *val); - - auto v_false = Value::MakeBoolean(false); - val = d.ReadValue(NT_BOOLEAN); - ASSERT_TRUE(static_cast(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(val)); - EXPECT_EQ(*v_double, *val); - - val = d.ReadValue(NT_DOUBLE); - ASSERT_TRUE(static_cast(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(val)); - EXPECT_EQ(*v_string, *val); - - auto v_bye = Value::MakeString("bye"sv); - val = d.ReadValue(NT_STRING); - ASSERT_TRUE(static_cast(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(val)); - EXPECT_EQ(*v_raw, *val); - - auto v_bye = Value::MakeRaw("bye"sv); - val = d.ReadValue(NT_RAW); - ASSERT_TRUE(static_cast(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(val)); - EXPECT_EQ(*v_boolean_array, *val); - - auto v_boolean_array2 = Value::MakeBooleanArray(std::vector{1, 0}); - val = d.ReadValue(NT_BOOLEAN_ARRAY); - ASSERT_TRUE(static_cast(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(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(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(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(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(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 b0cc664b8c..0000000000 --- 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 -#include -#include -#include - -#include - -#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(); - 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{0, 1, 0}); - v_boolean_array_big = Value::MakeBooleanArray(std::vector(256)); - v_double_array = Value::MakeDoubleArray(std::vector{0.5, 0.25}); - v_double_array_big = Value::MakeDoubleArray(std::vector(256)); - - std::vector 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 v_empty; - std::shared_ptr v_boolean, v_double, v_string, v_raw; - std::shared_ptr v_boolean_array, v_boolean_array_big; - std::shared_ptr v_double_array, v_double_array_big; - std::shared_ptr 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::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 a3fec50c42..caf7318925 100644 --- a/ntcore/src/test/native/cpp/main.cpp +++ b/ntcore/src/test/native/cpp/main.cpp @@ -19,3 +19,15 @@ int main(int argc, char** 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 0000000000..6e402faad0 --- /dev/null +++ b/ntcore/src/test/native/cpp/net/MockNetworkInterface.h @@ -0,0 +1,86 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source 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 + +#include + +#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 MockNetworkStartupInterface : public NetworkStartupInterface { + public: + MOCK_METHOD(void, Publish, + (NT_Publisher pubHandle, NT_Topic topicHandle, + std::string_view name, std::string_view typeStr, + const wpi::json& properties, const PubSubOptions& options), + (override)); + MOCK_METHOD(void, Subscribe, + (NT_Subscriber subHandle, wpi::span prefixes, + const PubSubOptions& options), + (override)); + MOCK_METHOD(void, SetValue, (NT_Publisher pubHandle, 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 PubSubOptions& 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, wpi::span prefixes, + const PubSubOptions& 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, (NetworkStartupInterface & startup), + (override)); + MOCK_METHOD(void, SetNetwork, (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 0000000000..a5ddb1fa9c --- /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 0000000000..36bbcbff21 --- /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 + +#include +#include +#include + +#include +#include + +#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, (wpi::span 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 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 0000000000..9af10ad9be --- /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 +#include + +#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, wpi::span prefixes, + const PubSubOptions& 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 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 handler; + StrictMock logger; +}; + +class WireDecodeTextServerTest : public ::testing::Test { + public: + StrictMock handler; + StrictMock 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 0000000000..f129c0ba3d --- /dev/null +++ b/ntcore/src/test/native/cpp/net/WireEncoderTest.cpp @@ -0,0 +1,292 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include +#include +#include + +#include +#include + +#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 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::vector{{"a", "b"}}, + PubSubOptions{}); + ASSERT_EQ(os.str(), + "{\"method\":\"subscribe\",\"params\":{" + "\"options\":{},\"topics\":[\"a\",\"b\"],\"subuid\":5}}"); +} + +TEST_F(WireEncoderTextTest, SubscribeSendAll) { + PubSubOptions options; + options.sendAll = true; + net::WireEncodeSubscribe(os, 5, std::vector{{"a", "b"}}, + options); + ASSERT_EQ(os.str(), + "{\"method\":\"subscribe\",\"params\":{" + "\"options\":{\"all\":true},\"topics\":[\"a\",\"b\"]," + "\"subuid\":5}}"); +} + +TEST_F(WireEncoderTextTest, SubscribePeriodic) { + PubSubOptions options; + options.periodic = 0.5; + net::WireEncodeSubscribe(os, 5, std::vector{{"a", "b"}}, + options); + ASSERT_EQ(os.str(), + "{\"method\":\"subscribe\",\"params\":{" + "\"options\":{\"periodic\":0.5},\"topics\":[\"a\",\"b\"]," + "\"subuid\":5}}"); +} + +TEST_F(WireEncoderTextTest, SubscribeAllOptions) { + PubSubOptions options; + options.sendAll = true; + options.periodic = 0.5; + net::WireEncodeSubscribe(os, 5, std::vector{{"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/MessageMatcher.cpp b/ntcore/src/test/native/cpp/net3/MessageMatcher3.cpp similarity index 59% rename from ntcore/src/test/native/cpp/MessageMatcher.cpp rename to ntcore/src/test/native/cpp/net3/MessageMatcher3.cpp index 69c87a00d4..d595c6ac1b 100644 --- a/ntcore/src/test/native/cpp/MessageMatcher.cpp +++ b/ntcore/src/test/native/cpp/net3/MessageMatcher3.cpp @@ -2,37 +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. -#include "MessageMatcher.h" +#include "MessageMatcher3.h" -namespace nt { +namespace nt::net3 { bool MessageMatcher::MatchAndExplain( - std::shared_ptr msg, - ::testing::MatchResultListener* listener) const { + Message3 msg, ::testing::MatchResultListener* listener) const { bool match = true; - if (!msg) { - return false; - } - if (msg->str() != goodmsg->str()) { + 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())) { + 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()) { + if (msg.id() != goodmsg.id()) { *listener << "id mismatch "; match = false; } - if (msg->flags() != goodmsg->flags()) { + if (msg.flags() != goodmsg.flags()) { *listener << "flags mismatch"; match = false; } - if (msg->seq_num_uid() != goodmsg->seq_num_uid()) { + if (msg.seq_num_uid() != goodmsg.seq_num_uid()) { *listener << "seq_num_uid mismatch"; match = false; } @@ -48,4 +42,4 @@ void MessageMatcher::DescribeNegationTo(::std::ostream* os) const { PrintTo(goodmsg, os); } -} // namespace nt +} // 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 0000000000..6b1e770dc5 --- /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 +#include +#include + +#include "../TestPrinters.h" +#include "gmock/gmock.h" +#include "net3/Message3.h" + +namespace nt::net3 { + +class MessageMatcher : public ::testing::MatcherInterface { + 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 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 0000000000..cc499d149a --- /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 + +#include + +#include +#include + +#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, (wpi::span 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 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 0000000000..8870aec0d7 --- /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 + +#include +#include +#include +#include + +#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, + wpi::span params)); + MOCK_METHOD3(RpcResponse, void(unsigned int id, unsigned int uid, + wpi::span result)); +}; + +class WireDecoder3Test : public ::testing::Test { + protected: + StrictMock handler; + net3::WireDecoder3 decoder{handler}; + + void DecodeComplete(wpi::span 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::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 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 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 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 in{0x11, 0x56, 0x78, 0x12, 0x34, 0x10, 0xff}; + in.insert(in.end(), 255, 0); + std::vector 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 in{0x11, 0x56, 0x78, 0x12, 0x34, 0x11, 0xff}; + in.insert(in.end(), 255 * 8, 0); + std::vector 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 in{0x11, 0x56, 0x78, 0x12, 0x34, 0x12, 0xff}; + in.insert(in.end(), 255, 0); + std::vector 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 0000000000..bb4bfd2cac --- /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 +#include +#include +#include +#include + +#include + +#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 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::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 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 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 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 ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x10, 0xff}; + ex.insert(ex.end(), 255, 0); + std::vector 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 ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x10, 0xff}; + ex.insert(ex.end(), 255, 0); + std::vector 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 ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x11, 0xff}; + ex.insert(ex.end(), 255 * 8, 0); + std::vector 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 ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x11, 0xff}; + ex.insert(ex.end(), 255 * 8, 0); + std::vector 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 ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x12, 0xff}; + ex.insert(ex.end(), 255, 0); + std::vector 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 ex{0x11, 0x56, 0x78, 0x12, 0x34, 0x12, 0xff}; + ex.insert(ex.end(), 255, 0); + std::vector in(256, ""); + net3::WireEncode( + os, net3::Message3::EntryUpdate(0x5678, 0x1234, + Value::MakeStringArray(std::move(in)))); + ASSERT_THAT(out, ex); +} + +} // namespace nt diff --git a/outlineviewer/build.gradle b/outlineviewer/build.gradle index cbf0a16c63..f7d18bcfe4 100644 --- a/outlineviewer/build.gradle +++ b/outlineviewer/build.gradle @@ -99,7 +99,7 @@ if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxar } lib project: ':glass', library: 'glassnt', linkage: 'static' lib project: ':glass', library: 'glass', linkage: 'static' - lib project: ':ntcore', library: 'ntcore', linkage: 'static' + project(':ntcore').addNtcoreDependency(it, 'static') lib project: ':wpinet', library: 'wpinet', linkage: 'static' lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' lib project: ':wpigui', library: 'wpigui', linkage: 'static' diff --git a/outlineviewer/src/main/native/cpp/main.cpp b/outlineviewer/src/main/native/cpp/main.cpp index 2c2debc197..b368cd3f3d 100644 --- a/outlineviewer/src/main/native/cpp/main.cpp +++ b/outlineviewer/src/main/native/cpp/main.cpp @@ -48,8 +48,7 @@ static void NtInitialize() { if (!win) { return; } - bool timedOut; - for (auto&& event : nt::PollConnectionListener(poller, 0, &timedOut)) { + for (auto&& event : nt::ReadConnectionListenerQueue(poller)) { if ((nt::GetNetworkMode(inst) & NT_NET_MODE_SERVER) != 0) { // for server mode, just print number of clients connected glfwSetWindowTitle(win, @@ -70,8 +69,7 @@ static void NtInitialize() { auto logPoller = nt::CreateLoggerPoller(inst); nt::AddPolledLogger(logPoller, NT_LOG_INFO, 100); gui::AddEarlyExecute([logPoller] { - bool timedOut; - for (auto&& msg : nt::PollLogger(logPoller, 0, &timedOut)) { + for (auto&& msg : nt::ReadLoggerQueue(logPoller)) { const char* level = ""; if (msg.level >= NT_LOG_CRITICAL) { level = "CRITICAL: "; @@ -91,6 +89,7 @@ static void NtInitialize() { // NetworkTables settings window gSettings = std::make_unique( + "outlineviewer", glass::GetStorageRoot().GetChild("NetworkTables Settings")); gui::AddEarlyExecute([] { gSettings->Update(); }); } @@ -193,6 +192,8 @@ static void DisplayGui() { } // display table view + glass::DisplayNetworkTablesInfo(gModel.get()); + ImGui::Separator(); glass::DisplayNetworkTables(gModel.get(), gFlagsSettings.GetFlags()); ImGui::End(); diff --git a/shared/jni/setupBuild.gradle b/shared/jni/setupBuild.gradle index 2110395073..de50d75349 100644 --- a/shared/jni/setupBuild.gradle +++ b/shared/jni/setupBuild.gradle @@ -42,6 +42,9 @@ model { cpp { source { srcDirs 'src/main/native/cpp' + if (project.hasProperty('generatedSources')) { + srcDir generatedSources + } include '**/*.cpp' exclude '**/jni/**/*.cpp' } @@ -110,6 +113,9 @@ model { cpp { source { srcDirs 'src/main/native/cpp' + if (project.hasProperty('generatedSources')) { + srcDir generatedSources + } include '**/jni/**/*.cpp' } exportedHeaders { @@ -151,6 +157,9 @@ model { cpp { source { srcDirs 'src/main/native/cpp' + if (project.hasProperty('generatedSources')) { + srcDir generatedSources + } include '**/jni/**/*.cpp' } exportedHeaders { diff --git a/simulation/halsim_gui/build.gradle b/simulation/halsim_gui/build.gradle index b9dcc316bf..fa4ccd957e 100644 --- a/simulation/halsim_gui/build.gradle +++ b/simulation/halsim_gui/build.gradle @@ -25,7 +25,7 @@ if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxar lib project: ':glass', library: 'glass', linkage: 'static' lib project: ':wpigui', library: 'wpigui', linkage: 'static' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' nativeUtils.useRequiredLibrary(it, 'imgui_static') diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp index 325673ac4e..30f123259e 100644 --- a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp @@ -16,10 +16,12 @@ using namespace halsimgui; static std::unique_ptr gNetworkTablesModel; static std::unique_ptr gNetworkTablesWindow; +static std::unique_ptr gNetworkTablesInfoWindow; void NetworkTablesSimGui::Initialize() { gNetworkTablesModel = std::make_unique(); wpi::gui::AddEarlyExecute([] { gNetworkTablesModel->Update(); }); + gNetworkTablesWindow = std::make_unique( glass::GetStorageRoot().GetChild("NetworkTables View"), "NetworkTables"); gNetworkTablesWindow->SetView( @@ -29,9 +31,22 @@ void NetworkTablesSimGui::Initialize() { gNetworkTablesWindow->DisableRenamePopup(); wpi::gui::AddLateExecute([] { gNetworkTablesWindow->Display(); }); + // NetworkTables info window + gNetworkTablesInfoWindow = std::make_unique( + glass::GetStorageRoot().GetChild("NetworkTables Info"), + "NetworkTables Info"); + gNetworkTablesInfoWindow->SetView(glass::MakeFunctionView( + [&] { glass::DisplayNetworkTablesInfo(gNetworkTablesModel.get()); })); + gNetworkTablesInfoWindow->SetDefaultPos(250, 130); + gNetworkTablesInfoWindow->SetDefaultSize(750, 145); + gNetworkTablesInfoWindow->SetDefaultVisibility(glass::Window::kHide); + gNetworkTablesInfoWindow->DisableRenamePopup(); + wpi::gui::AddLateExecute([] { gNetworkTablesInfoWindow->Display(); }); + wpi::gui::AddWindowScaler([](float scale) { // scale default window positions gNetworkTablesWindow->ScaleDefault(scale); + gNetworkTablesInfoWindow->ScaleDefault(scale); }); } @@ -39,4 +54,7 @@ void NetworkTablesSimGui::DisplayMenu() { if (gNetworkTablesWindow) { gNetworkTablesWindow->DisplayMenuItem("NetworkTables View"); } + if (gNetworkTablesInfoWindow) { + gNetworkTablesInfoWindow->DisplayMenuItem("NetworkTables Info"); + } } diff --git a/styleguide/spotbugs-exclude.xml b/styleguide/spotbugs-exclude.xml index 7b83038a9e..0c0156d346 100644 --- a/styleguide/spotbugs-exclude.xml +++ b/styleguide/spotbugs-exclude.xml @@ -4,6 +4,10 @@ + + + + @@ -72,6 +76,10 @@ + + + + @@ -79,6 +87,9 @@ + + + @@ -107,4 +118,8 @@ + + + + diff --git a/wpilibNewCommands/build.gradle b/wpilibNewCommands/build.gradle index df85ed5ded..82fc470036 100644 --- a/wpilibNewCommands/build.gradle +++ b/wpilibNewCommands/build.gradle @@ -57,14 +57,14 @@ model { return } lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') project(':hal').addHalDependency(it, 'shared') lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' if (it.component.name == "${nativeName}Dev") { - lib project: ':ntcore', library: 'ntcoreJNIShared', linkage: 'shared' + project(':ntcore').addNtcoreJniDependency(it) lib project: ':wpinet', library: 'wpinetJNIShared', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutilJNIShared', linkage: 'shared' project(':hal').addHalJniDependency(it) diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/CommandScheduler.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/CommandScheduler.java index 5026385d92..5d72ddfa31 100644 --- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/CommandScheduler.java +++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/CommandScheduler.java @@ -9,9 +9,13 @@ import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; import edu.wpi.first.hal.FRCNetComm.tInstances; import edu.wpi.first.hal.FRCNetComm.tResourceType; import edu.wpi.first.hal.HAL; +import edu.wpi.first.networktables.IntegerArrayEntry; +import edu.wpi.first.networktables.IntegerArrayPublisher; +import edu.wpi.first.networktables.IntegerArrayTopic; import edu.wpi.first.networktables.NTSendable; import edu.wpi.first.networktables.NTSendableBuilder; -import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.StringArrayPublisher; +import edu.wpi.first.networktables.StringArrayTopic; import edu.wpi.first.util.sendable.SendableRegistry; import edu.wpi.first.wpilibj.DriverStation; import edu.wpi.first.wpilibj.RobotBase; @@ -540,36 +544,43 @@ public final class CommandScheduler implements NTSendable, AutoCloseable { @Override public void initSendable(NTSendableBuilder builder) { builder.setSmartDashboardType("Scheduler"); - final NetworkTableEntry namesEntry = builder.getEntry("Names"); - final NetworkTableEntry idsEntry = builder.getEntry("Ids"); - final NetworkTableEntry cancelEntry = builder.getEntry("Cancel"); + final StringArrayPublisher namesPub = new StringArrayTopic(builder.getTopic("Names")).publish(); + final IntegerArrayPublisher idsPub = new IntegerArrayTopic(builder.getTopic("Ids")).publish(); + final IntegerArrayEntry cancelEntry = + new IntegerArrayTopic(builder.getTopic("Cancel")).getEntry(new long[] {}); + builder.addCloseable(namesPub); + builder.addCloseable(idsPub); + builder.addCloseable(cancelEntry); builder.setUpdateTable( () -> { - if (namesEntry == null || idsEntry == null || cancelEntry == null) { + if (namesPub == null || idsPub == null || cancelEntry == null) { return; } - Map ids = new LinkedHashMap<>(); + Map ids = new LinkedHashMap<>(); + List names = new ArrayList<>(); + long[] ids2 = new long[m_scheduledCommands.size()]; + int i = 0; for (Command command : m_scheduledCommands) { - ids.put((double) command.hashCode(), command); + long id = command.hashCode(); + ids.put(id, command); + names.add(command.getName()); + ids2[i] = id; + i++; } - double[] toCancel = cancelEntry.getDoubleArray(new double[0]); + long[] toCancel = cancelEntry.get(); if (toCancel.length > 0) { - for (double hash : toCancel) { + for (long hash : toCancel) { cancel(ids.get(hash)); ids.remove(hash); } - cancelEntry.setDoubleArray(new double[0]); + cancelEntry.set(new long[] {}); } - List names = new ArrayList<>(); - - ids.values().forEach(command -> names.add(command.getName())); - - namesEntry.setStringArray(names.toArray(new String[0])); - idsEntry.setNumberArray(ids.keySet().toArray(new Double[0])); + namesPub.set(names.toArray(new String[] {})); + idsPub.set(ids2); }); } } diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/NetworkButton.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/NetworkButton.java index 8eb7d445ad..dafe471b46 100644 --- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/NetworkButton.java +++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/NetworkButton.java @@ -6,8 +6,9 @@ package edu.wpi.first.wpilibj2.command.button; import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam; +import edu.wpi.first.networktables.BooleanSubscriber; +import edu.wpi.first.networktables.BooleanTopic; import edu.wpi.first.networktables.NetworkTable; -import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.networktables.NetworkTableInstance; /** @@ -19,11 +20,20 @@ public class NetworkButton extends Button { /** * Creates a NetworkButton that commands can be bound to. * - * @param entry The entry that is the value. + * @param topic The boolean topic that contains the value. */ - public NetworkButton(NetworkTableEntry entry) { - super(() -> entry.getInstance().isConnected() && entry.getBoolean(false)); - requireNonNullParam(entry, "entry", "NetworkButton"); + public NetworkButton(BooleanTopic topic) { + this(topic.subscribe(false)); + } + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param sub The boolean subscriber that provides the value. + */ + public NetworkButton(BooleanSubscriber sub) { + super(() -> sub.getTopic().getInstance().isConnected() && sub.get()); + requireNonNullParam(sub, "sub", "NetworkButton"); } /** @@ -33,7 +43,7 @@ public class NetworkButton extends Button { * @param field The field that is the value. */ public NetworkButton(NetworkTable table, String field) { - this(table.getEntry(field)); + this(table.getBooleanTopic(field)); } /** @@ -43,6 +53,17 @@ public class NetworkButton extends Button { * @param field The field that is the value. */ public NetworkButton(String table, String field) { - this(NetworkTableInstance.getDefault().getTable(table), field); + this(NetworkTableInstance.getDefault(), table, field); + } + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param inst The NetworkTable instance to use + * @param table The table where the networktable value is located. + * @param field The field that is the value. + */ + public NetworkButton(NetworkTableInstance inst, String table, String field) { + this(inst.getTable(table), field); } } diff --git a/wpilibNewCommands/src/main/native/cpp/frc2/command/CommandScheduler.cpp b/wpilibNewCommands/src/main/native/cpp/frc2/command/CommandScheduler.cpp index db34d49ccc..b4a6292706 100644 --- a/wpilibNewCommands/src/main/native/cpp/frc2/command/CommandScheduler.cpp +++ b/wpilibNewCommands/src/main/native/cpp/frc2/command/CommandScheduler.cpp @@ -12,8 +12,9 @@ #include #include #include +#include #include -#include +#include #include #include #include @@ -418,34 +419,35 @@ void CommandScheduler::OnCommandFinish(Action action) { void CommandScheduler::InitSendable(nt::NTSendableBuilder& builder) { builder.SetSmartDashboardType("Scheduler"); - auto namesEntry = builder.GetEntry("Names"); - auto idsEntry = builder.GetEntry("Ids"); - auto cancelEntry = builder.GetEntry("Cancel"); + builder.SetUpdateTable( + [this, + namesPub = nt::StringArrayTopic{builder.GetTopic("Names")}.Publish(), + idsPub = nt::IntegerArrayTopic{builder.GetTopic("Ids")}.Publish(), + cancelEntry = nt::IntegerArrayTopic{builder.GetTopic("Cancel")}.GetEntry( + {})]() mutable { + auto toCancel = cancelEntry.Get(); + if (!toCancel.empty()) { + for (auto cancel : cancelEntry.Get()) { + uintptr_t ptrTmp = static_cast(cancel); + Command* command = reinterpret_cast(ptrTmp); + if (m_impl->scheduledCommands.find(command) != + m_impl->scheduledCommands.end()) { + Cancel(command); + } + } + cancelEntry.Set({}); + } - builder.SetUpdateTable([=] { - double tmp[1]; - tmp[0] = 0; - auto toCancel = cancelEntry.GetDoubleArray(tmp); - for (auto cancel : toCancel) { - uintptr_t ptrTmp = static_cast(cancel); - Command* command = reinterpret_cast(ptrTmp); - if (m_impl->scheduledCommands.find(command) != - m_impl->scheduledCommands.end()) { - Cancel(command); - } - nt::NetworkTableEntry(cancelEntry).SetDoubleArray({}); - } - - wpi::SmallVector names; - wpi::SmallVector ids; - for (Command* command : m_impl->scheduledCommands) { - names.emplace_back(command->GetName()); - uintptr_t ptrTmp = reinterpret_cast(command); - ids.emplace_back(static_cast(ptrTmp)); - } - nt::NetworkTableEntry(namesEntry).SetStringArray(names); - nt::NetworkTableEntry(idsEntry).SetDoubleArray(ids); - }); + wpi::SmallVector names; + wpi::SmallVector ids; + for (Command* command : m_impl->scheduledCommands) { + names.emplace_back(command->GetName()); + uintptr_t ptrTmp = reinterpret_cast(command); + ids.emplace_back(static_cast(ptrTmp)); + } + namesPub.Set(names); + idsPub.Set(ids); + }); } void CommandScheduler::SetDefaultCommandImpl(Subsystem* subsystem, diff --git a/wpilibNewCommands/src/main/native/cpp/frc2/command/button/NetworkButton.cpp b/wpilibNewCommands/src/main/native/cpp/frc2/command/button/NetworkButton.cpp index 3886c625d0..758044938b 100644 --- a/wpilibNewCommands/src/main/native/cpp/frc2/command/button/NetworkButton.cpp +++ b/wpilibNewCommands/src/main/native/cpp/frc2/command/button/NetworkButton.cpp @@ -6,15 +6,21 @@ using namespace frc2; -NetworkButton::NetworkButton(nt::NetworkTableEntry entry) - : Button([entry] { - return entry.GetInstance().IsConnected() && entry.GetBoolean(false); +NetworkButton::NetworkButton(nt::BooleanTopic topic) + : NetworkButton(topic.Subscribe(false)) {} + +NetworkButton::NetworkButton(nt::BooleanSubscriber sub) + : Button([sub = std::make_shared(std::move(sub))] { + return sub->GetTopic().GetInstance().IsConnected() && sub->Get(); }) {} NetworkButton::NetworkButton(std::shared_ptr table, std::string_view field) - : NetworkButton(table->GetEntry(field)) {} + : NetworkButton(table->GetBooleanTopic(field)) {} NetworkButton::NetworkButton(std::string_view table, std::string_view field) - : NetworkButton(nt::NetworkTableInstance::GetDefault().GetTable(table), - field) {} + : NetworkButton(nt::NetworkTableInstance::GetDefault(), table, field) {} + +NetworkButton::NetworkButton(nt::NetworkTableInstance inst, + std::string_view table, std::string_view field) + : NetworkButton(inst.GetTable(table), field) {} diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/button/NetworkButton.h b/wpilibNewCommands/src/main/native/include/frc2/command/button/NetworkButton.h index de5044ba0b..3d0378d6bb 100644 --- a/wpilibNewCommands/src/main/native/include/frc2/command/button/NetworkButton.h +++ b/wpilibNewCommands/src/main/native/include/frc2/command/button/NetworkButton.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -23,9 +24,16 @@ class NetworkButton : public Button { /** * Creates a NetworkButton that commands can be bound to. * - * @param entry The entry that is the value. + * @param topic The boolean topic that contains the value. */ - explicit NetworkButton(nt::NetworkTableEntry entry); + explicit NetworkButton(nt::BooleanTopic topic); + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param sub The boolean subscriber that provides the value. + */ + explicit NetworkButton(nt::BooleanSubscriber sub); /** * Creates a NetworkButton that commands can be bound to. @@ -43,5 +51,15 @@ class NetworkButton : public Button { * @param field The field that is the value. */ NetworkButton(std::string_view table, std::string_view field); + + /** + * Creates a NetworkButton that commands can be bound to. + * + * @param inst The NetworkTable instance to use + * @param table The table where the networktable value is located. + * @param field The field that is the value. + */ + NetworkButton(nt::NetworkTableInstance inst, std::string_view table, + std::string_view field); }; } // namespace frc2 diff --git a/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/NetworkButtonTest.java b/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/NetworkButtonTest.java index 14513d79b0..c894cff052 100644 --- a/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/NetworkButtonTest.java +++ b/wpilibNewCommands/src/test/java/edu/wpi/first/wpilibj2/command/button/NetworkButtonTest.java @@ -15,15 +15,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class NetworkButtonTest extends CommandTestBase { + NetworkTableInstance m_inst; + @BeforeEach void setup() { - NetworkTableInstance.getDefault().startLocal(); + m_inst = NetworkTableInstance.create(); + m_inst.startLocal(); } @AfterEach void teardown() { - NetworkTableInstance.getDefault().deleteAllEntries(); - NetworkTableInstance.getDefault().stopLocal(); + m_inst.close(); } @Test @@ -31,14 +33,14 @@ class NetworkButtonTest extends CommandTestBase { var scheduler = CommandScheduler.getInstance(); var commandHolder = new MockCommandHolder(true); var command = commandHolder.getMock(); - var entry = NetworkTableInstance.getDefault().getTable("TestTable").getEntry("Test"); + var pub = m_inst.getTable("TestTable").getBooleanTopic("Test").publish(); - var button = new NetworkButton("TestTable", "Test"); - entry.setBoolean(false); + var button = new NetworkButton(m_inst, "TestTable", "Test"); + pub.set(false); button.whenPressed(command); scheduler.run(); verify(command, never()).schedule(); - entry.setBoolean(true); + pub.set(true); scheduler.run(); scheduler.run(); verify(command).schedule(); diff --git a/wpilibNewCommands/src/test/native/cpp/frc2/command/button/NetworkButtonTest.cpp b/wpilibNewCommands/src/test/native/cpp/frc2/command/button/NetworkButtonTest.cpp index 7de10843b9..95a7eee628 100644 --- a/wpilibNewCommands/src/test/native/cpp/frc2/command/button/NetworkButtonTest.cpp +++ b/wpilibNewCommands/src/test/native/cpp/frc2/command/button/NetworkButtonTest.cpp @@ -14,27 +14,28 @@ using namespace frc2; class NetworkButtonTest : public CommandTestBase { - void SetUp() override { nt::NetworkTableInstance::GetDefault().StartLocal(); } - - void TearDown() override { - nt::NetworkTableInstance::GetDefault().DeleteAllEntries(); - nt::NetworkTableInstance::GetDefault().StopLocal(); + public: + NetworkButtonTest() { + inst = nt::NetworkTableInstance::Create(); + inst.StartLocal(); } + + ~NetworkButtonTest() override { nt::NetworkTableInstance::Destroy(inst); } + + nt::NetworkTableInstance inst; }; TEST_F(NetworkButtonTest, SetNetworkButton) { auto& scheduler = CommandScheduler::GetInstance(); - auto entry = nt::NetworkTableInstance::GetDefault() - .GetTable("TestTable") - ->GetEntry("Test"); + auto pub = inst.GetTable("TestTable")->GetBooleanTopic("Test").Publish(); bool finished = false; WaitUntilCommand command([&finished] { return finished; }); - NetworkButton("TestTable", "Test").WhenActive(&command); + NetworkButton(inst, "TestTable", "Test").WhenActive(&command); scheduler.Run(); EXPECT_FALSE(scheduler.IsScheduled(&command)); - entry.SetBoolean(true); + pub.Set(true); scheduler.Run(); EXPECT_TRUE(scheduler.IsScheduled(&command)); finished = true; diff --git a/wpilibc/build.gradle b/wpilibc/build.gradle index 5d8b299253..9186f80a1c 100644 --- a/wpilibc/build.gradle +++ b/wpilibc/build.gradle @@ -101,7 +101,7 @@ model { return } cppCompiler.define 'DYNAMIC_CAMERA_SERVER' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') project(':hal').addHalDependency(it, 'shared') lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' @@ -121,7 +121,7 @@ model { } } binaries.all { - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') project(':hal').addHalDependency(it, 'shared') lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' @@ -158,7 +158,7 @@ model { } } binaries.all { - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') lib project: ':cscore', library: 'cscore', linkage: 'shared' project(':hal').addHalDependency(it, 'shared') lib project: ':wpinet', library: 'wpinet', linkage: 'shared' @@ -209,7 +209,7 @@ model { } } withType(GoogleTestTestSuiteBinarySpec) { - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') lib project: ':cscore', library: 'cscore', linkage: 'shared' project(':hal').addHalDependency(it, 'shared') lib project: ':wpinet', library: 'wpinet', linkage: 'shared' diff --git a/wpilibc/src/main/native/cpp/ADIS16448_IMU.cpp b/wpilibc/src/main/native/cpp/ADIS16448_IMU.cpp index 72d0630549..53f4b31bed 100644 --- a/wpilibc/src/main/native/cpp/ADIS16448_IMU.cpp +++ b/wpilibc/src/main/native/cpp/ADIS16448_IMU.cpp @@ -873,8 +873,6 @@ int ADIS16448_IMU::GetPort() const { **/ void ADIS16448_IMU::InitSendable(nt::NTSendableBuilder& builder) { builder.SetSmartDashboardType("ADIS16448 IMU"); - auto yaw_angle = builder.GetEntry("Yaw Angle").GetHandle(); - builder.SetUpdateTable([=]() { - nt::NetworkTableEntry(yaw_angle).SetDouble(GetAngle().value()); - }); + builder.AddDoubleProperty( + "Yaw Angle", [=] { return GetAngle().value(); }, nullptr); } diff --git a/wpilibc/src/main/native/cpp/ADIS16470_IMU.cpp b/wpilibc/src/main/native/cpp/ADIS16470_IMU.cpp index f2ff744aa1..6661045f72 100644 --- a/wpilibc/src/main/native/cpp/ADIS16470_IMU.cpp +++ b/wpilibc/src/main/native/cpp/ADIS16470_IMU.cpp @@ -808,8 +808,6 @@ int ADIS16470_IMU::GetPort() const { **/ void ADIS16470_IMU::InitSendable(nt::NTSendableBuilder& builder) { builder.SetSmartDashboardType("ADIS16470 IMU"); - auto yaw_angle = builder.GetEntry("Yaw Angle").GetHandle(); - builder.SetUpdateTable([=]() { - nt::NetworkTableEntry(yaw_angle).SetDouble(GetAngle().value()); - }); + builder.AddDoubleProperty( + "Yaw Angle", [=] { return GetAngle().value(); }, nullptr); } diff --git a/wpilibc/src/main/native/cpp/ADXL345_I2C.cpp b/wpilibc/src/main/native/cpp/ADXL345_I2C.cpp index 484e69084d..a23846d829 100644 --- a/wpilibc/src/main/native/cpp/ADXL345_I2C.cpp +++ b/wpilibc/src/main/native/cpp/ADXL345_I2C.cpp @@ -5,6 +5,7 @@ #include "frc/ADXL345_I2C.h" #include +#include #include #include @@ -93,13 +94,13 @@ ADXL345_I2C::AllAxes ADXL345_I2C::GetAccelerations() { void ADXL345_I2C::InitSendable(nt::NTSendableBuilder& builder) { builder.SetSmartDashboardType("3AxisAccelerometer"); - auto x = builder.GetEntry("X").GetHandle(); - auto y = builder.GetEntry("Y").GetHandle(); - auto z = builder.GetEntry("Z").GetHandle(); - builder.SetUpdateTable([=] { - auto data = GetAccelerations(); - nt::NetworkTableEntry(x).SetDouble(data.XAxis); - nt::NetworkTableEntry(y).SetDouble(data.YAxis); - nt::NetworkTableEntry(z).SetDouble(data.ZAxis); - }); + builder.SetUpdateTable( + [this, x = nt::DoubleTopic{builder.GetTopic("X")}.Publish(), + y = nt::DoubleTopic{builder.GetTopic("Y")}.Publish(), + z = nt::DoubleTopic{builder.GetTopic("Z")}.Publish()]() mutable { + auto data = GetAccelerations(); + x.Set(data.XAxis); + y.Set(data.YAxis); + z.Set(data.ZAxis); + }); } diff --git a/wpilibc/src/main/native/cpp/ADXL345_SPI.cpp b/wpilibc/src/main/native/cpp/ADXL345_SPI.cpp index d3df40017a..1dc4c5b165 100644 --- a/wpilibc/src/main/native/cpp/ADXL345_SPI.cpp +++ b/wpilibc/src/main/native/cpp/ADXL345_SPI.cpp @@ -5,6 +5,7 @@ #include "frc/ADXL345_SPI.h" #include +#include #include #include @@ -118,13 +119,13 @@ ADXL345_SPI::AllAxes ADXL345_SPI::GetAccelerations() { void ADXL345_SPI::InitSendable(nt::NTSendableBuilder& builder) { builder.SetSmartDashboardType("3AxisAccelerometer"); - auto x = builder.GetEntry("X").GetHandle(); - auto y = builder.GetEntry("Y").GetHandle(); - auto z = builder.GetEntry("Z").GetHandle(); - builder.SetUpdateTable([=] { - auto data = GetAccelerations(); - nt::NetworkTableEntry(x).SetDouble(data.XAxis); - nt::NetworkTableEntry(y).SetDouble(data.YAxis); - nt::NetworkTableEntry(z).SetDouble(data.ZAxis); - }); + builder.SetUpdateTable( + [this, x = nt::DoubleTopic{builder.GetTopic("X")}.Publish(), + y = nt::DoubleTopic{builder.GetTopic("Y")}.Publish(), + z = nt::DoubleTopic{builder.GetTopic("Z")}.Publish()]() mutable { + auto data = GetAccelerations(); + x.Set(data.XAxis); + y.Set(data.YAxis); + z.Set(data.ZAxis); + }); } diff --git a/wpilibc/src/main/native/cpp/ADXL362.cpp b/wpilibc/src/main/native/cpp/ADXL362.cpp index be4afb7b9c..4e59ba9a7a 100644 --- a/wpilibc/src/main/native/cpp/ADXL362.cpp +++ b/wpilibc/src/main/native/cpp/ADXL362.cpp @@ -5,6 +5,7 @@ #include "frc/ADXL362.h" #include +#include #include #include @@ -182,13 +183,13 @@ ADXL362::AllAxes ADXL362::GetAccelerations() { void ADXL362::InitSendable(nt::NTSendableBuilder& builder) { builder.SetSmartDashboardType("3AxisAccelerometer"); - auto x = builder.GetEntry("X").GetHandle(); - auto y = builder.GetEntry("Y").GetHandle(); - auto z = builder.GetEntry("Z").GetHandle(); - builder.SetUpdateTable([=] { - auto data = GetAccelerations(); - nt::NetworkTableEntry(x).SetDouble(data.XAxis); - nt::NetworkTableEntry(y).SetDouble(data.YAxis); - nt::NetworkTableEntry(z).SetDouble(data.ZAxis); - }); + builder.SetUpdateTable( + [this, x = nt::DoubleTopic{builder.GetTopic("X")}.Publish(), + y = nt::DoubleTopic{builder.GetTopic("Y")}.Publish(), + z = nt::DoubleTopic{builder.GetTopic("Z")}.Publish()]() mutable { + auto data = GetAccelerations(); + x.Set(data.XAxis); + y.Set(data.YAxis); + z.Set(data.ZAxis); + }); } diff --git a/wpilibc/src/main/native/cpp/DriverStation.cpp b/wpilibc/src/main/native/cpp/DriverStation.cpp index a929ff4200..b7c0800f7b 100644 --- a/wpilibc/src/main/native/cpp/DriverStation.cpp +++ b/wpilibc/src/main/native/cpp/DriverStation.cpp @@ -19,9 +19,11 @@ #include #include #include +#include +#include #include -#include #include +#include #include #include #include @@ -36,56 +38,42 @@ using namespace frc; namespace { // A simple class which caches the previous value written to an NT entry // Used to prevent redundant, repeated writes of the same value -template +template class MatchDataSenderEntry { public: MatchDataSenderEntry(const std::shared_ptr& table, - std::string_view key, const T& initialVal) { - static_assert(std::is_same_v || std::is_same_v || - std::is_same_v, - "Invalid type for MatchDataSenderEntry - must be " - "to bool, double or std::string"); - - ntEntry = table->GetEntry(key); - if constexpr (std::is_same_v) { - ntEntry.ForceSetBoolean(initialVal); - } else if constexpr (std::is_same_v) { - ntEntry.ForceSetDouble(initialVal); - } else if constexpr (std::is_same_v) { - ntEntry.ForceSetString(initialVal); - } - prevVal = initialVal; + std::string_view key, + typename Topic::ParamType initialVal) + : publisher{Topic{table->GetTopic(key)}.Publish()}, prevVal{initialVal} { + publisher.Set(initialVal); } - void Set(const T& val) { + void Set(typename Topic::ParamType val) { if (val != prevVal) { - SetValue(val); + publisher.Set(val); prevVal = val; } } private: - nt::NetworkTableEntry ntEntry; - T prevVal; - - void SetValue(bool val) { ntEntry.SetBoolean(val); } - void SetValue(double val) { ntEntry.SetDouble(val); } - void SetValue(std::string_view val) { ntEntry.SetString(val); } + typename Topic::PublisherType publisher; + typename Topic::ValueType prevVal; }; struct MatchDataSender { std::shared_ptr table = nt::NetworkTableInstance::GetDefault().GetTable("FMSInfo"); - MatchDataSenderEntry typeMetaData{table, ".type", "FMSInfo"}; - MatchDataSenderEntry gameSpecificMessage{ + MatchDataSenderEntry typeMetaData{table, ".type", "FMSInfo"}; + MatchDataSenderEntry gameSpecificMessage{ table, "GameSpecificMessage", ""}; - MatchDataSenderEntry eventName{table, "EventName", ""}; - MatchDataSenderEntry matchNumber{table, "MatchNumber", 0.0}; - MatchDataSenderEntry replayNumber{table, "ReplayNumber", 0.0}; - MatchDataSenderEntry matchType{table, "MatchType", 0.0}; - MatchDataSenderEntry alliance{table, "IsRedAlliance", true}; - MatchDataSenderEntry station{table, "StationNumber", 1.0}; - MatchDataSenderEntry controlWord{table, "FMSControlData", 0.0}; + MatchDataSenderEntry eventName{table, "EventName", ""}; + MatchDataSenderEntry matchNumber{table, "MatchNumber", 0}; + MatchDataSenderEntry replayNumber{table, "ReplayNumber", 0}; + MatchDataSenderEntry matchType{table, "MatchType", 0}; + MatchDataSenderEntry alliance{table, "IsRedAlliance", true}; + MatchDataSenderEntry station{table, "StationNumber", 1}; + MatchDataSenderEntry controlWord{table, "FMSControlData", + 0}; }; class JoystickLogSender { diff --git a/wpilibc/src/main/native/cpp/IterativeRobotBase.cpp b/wpilibc/src/main/native/cpp/IterativeRobotBase.cpp index 9396f68c6d..3732dd0f67 100644 --- a/wpilibc/src/main/native/cpp/IterativeRobotBase.cpp +++ b/wpilibc/src/main/native/cpp/IterativeRobotBase.cpp @@ -187,7 +187,7 @@ void IterativeRobotBase::LoopFunc() { // Flush NetworkTables if (m_ntFlushEnabled) { - nt::NetworkTableInstance::GetDefault().Flush(); + nt::NetworkTableInstance::GetDefault().FlushLocal(); } // Warn on loop time overruns diff --git a/wpilibc/src/main/native/cpp/Preferences.cpp b/wpilibc/src/main/native/cpp/Preferences.cpp index 53bd5d68c6..a76ebfe46a 100644 --- a/wpilibc/src/main/native/cpp/Preferences.cpp +++ b/wpilibc/src/main/native/cpp/Preferences.cpp @@ -6,9 +6,13 @@ #include +#include #include +#include #include #include +#include +#include using namespace frc; @@ -21,7 +25,10 @@ struct Instance { std::shared_ptr table{ nt::NetworkTableInstance::GetDefault().GetTable(kTableName)}; - NT_EntryListener listener; + nt::StringPublisher typePublisher{table->GetStringTopic(".type").Publish()}; + nt::MultiSubscriber tableSubscriber{nt::NetworkTableInstance::GetDefault(), + {{kTableName}}}; + nt::TopicListener listener; }; } // namespace @@ -44,28 +51,27 @@ std::vector Preferences::GetKeys() { std::string Preferences::GetString(std::string_view key, std::string_view defaultValue) { - return ::GetInstance().table->GetString(key, defaultValue); + return ::GetInstance().table->GetEntry(key).GetString(defaultValue); } int Preferences::GetInt(std::string_view key, int defaultValue) { - return static_cast(::GetInstance().table->GetNumber(key, defaultValue)); + return ::GetInstance().table->GetEntry(key).GetInteger(defaultValue); } double Preferences::GetDouble(std::string_view key, double defaultValue) { - return ::GetInstance().table->GetNumber(key, defaultValue); + return ::GetInstance().table->GetEntry(key).GetDouble(defaultValue); } float Preferences::GetFloat(std::string_view key, float defaultValue) { - return ::GetInstance().table->GetNumber(key, defaultValue); + return ::GetInstance().table->GetEntry(key).GetFloat(defaultValue); } bool Preferences::GetBoolean(std::string_view key, bool defaultValue) { - return ::GetInstance().table->GetBoolean(key, defaultValue); + return ::GetInstance().table->GetEntry(key).GetBoolean(defaultValue); } int64_t Preferences::GetLong(std::string_view key, int64_t defaultValue) { - return static_cast( - ::GetInstance().table->GetNumber(key, defaultValue)); + return ::GetInstance().table->GetEntry(key).GetInteger(defaultValue); } void Preferences::SetString(std::string_view key, std::string_view value) { @@ -82,13 +88,13 @@ void Preferences::InitString(std::string_view key, std::string_view value) { void Preferences::SetInt(std::string_view key, int value) { auto entry = ::GetInstance().table->GetEntry(key); - entry.SetDouble(value); + entry.SetInteger(value); entry.SetPersistent(); } void Preferences::InitInt(std::string_view key, int value) { auto entry = ::GetInstance().table->GetEntry(key); - entry.SetDefaultDouble(value); + entry.SetDefaultInteger(value); entry.SetPersistent(); } @@ -106,13 +112,13 @@ void Preferences::InitDouble(std::string_view key, double value) { void Preferences::SetFloat(std::string_view key, float value) { auto entry = ::GetInstance().table->GetEntry(key); - entry.SetDouble(value); + entry.SetFloat(value); entry.SetPersistent(); } void Preferences::InitFloat(std::string_view key, float value) { auto entry = ::GetInstance().table->GetEntry(key); - entry.SetDefaultDouble(value); + entry.SetDefaultFloat(value); entry.SetPersistent(); } @@ -130,13 +136,13 @@ void Preferences::InitBoolean(std::string_view key, bool value) { void Preferences::SetLong(std::string_view key, int64_t value) { auto entry = ::GetInstance().table->GetEntry(key); - entry.SetDouble(value); + entry.SetInteger(value); entry.SetPersistent(); } void Preferences::InitLong(std::string_view key, int64_t value) { auto entry = ::GetInstance().table->GetEntry(key); - entry.SetDefaultDouble(value); + entry.SetDefaultInteger(value); entry.SetPersistent(); } @@ -145,7 +151,9 @@ bool Preferences::ContainsKey(std::string_view key) { } void Preferences::Remove(std::string_view key) { - ::GetInstance().table->Delete(key); + auto entry = ::GetInstance().table->GetEntry(key); + entry.ClearPersistent(); + entry.Unpublish(); } void Preferences::RemoveAll() { @@ -157,11 +165,13 @@ void Preferences::RemoveAll() { } Instance::Instance() { - table->GetEntry(".type").SetString("RobotPreferences"); - listener = table->AddEntryListener( - [=](nt::NetworkTable* table, std::string_view name, - nt::NetworkTableEntry entry, std::shared_ptr value, - int flags) { entry.SetPersistent(); }, - NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE); + typePublisher.Set("RobotPreferences"); + listener = nt::TopicListener{ + table->GetInstance(), + {{std::string_view{fmt::format("{}/", table->GetPath())}}}, + NT_TOPIC_NOTIFY_IMMEDIATE | NT_TOPIC_NOTIFY_PUBLISH, + [](const nt::TopicNotification& event) { + nt::SetTopicPersistent(event.info.topic, true); + }}; HAL_Report(HALUsageReporting::kResourceType_Preferences, 0); } diff --git a/wpilibc/src/main/native/cpp/event/NetworkBooleanEvent.cpp b/wpilibc/src/main/native/cpp/event/NetworkBooleanEvent.cpp new file mode 100644 index 0000000000..d910361b64 --- /dev/null +++ b/wpilibc/src/main/native/cpp/event/NetworkBooleanEvent.cpp @@ -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. + +#include "frc/event/NetworkBooleanEvent.h" + +#include +#include +#include + +using namespace frc; + +NetworkBooleanEvent::NetworkBooleanEvent(EventLoop* loop, + nt::BooleanTopic topic) + : NetworkBooleanEvent{loop, topic.Subscribe(false)} {} + +NetworkBooleanEvent::NetworkBooleanEvent(EventLoop* loop, + nt::BooleanSubscriber sub) + : BooleanEvent{ + loop, + [sub = std::make_shared(std::move(sub))] { + return sub->GetTopic().GetInstance().IsConnected() && sub->Get(); + }} {} + +NetworkBooleanEvent::NetworkBooleanEvent( + EventLoop* loop, std::shared_ptr table, + std::string_view topicName) + : NetworkBooleanEvent{loop, table->GetBooleanTopic(topicName)} {} + +NetworkBooleanEvent::NetworkBooleanEvent(EventLoop* loop, + std::string_view tableName, + std::string_view topicName) + : NetworkBooleanEvent{loop, nt::NetworkTableInstance::GetDefault(), + tableName, topicName} {} + +NetworkBooleanEvent::NetworkBooleanEvent(EventLoop* loop, + nt::NetworkTableInstance inst, + std::string_view tableName, + std::string_view topicName) + : NetworkBooleanEvent{loop, inst.GetTable(tableName), topicName} {} diff --git a/wpilibc/src/main/native/cpp/livewindow/LiveWindow.cpp b/wpilibc/src/main/native/cpp/livewindow/LiveWindow.cpp index 2adc7ce07e..c6a926e86e 100644 --- a/wpilibc/src/main/native/cpp/livewindow/LiveWindow.cpp +++ b/wpilibc/src/main/native/cpp/livewindow/LiveWindow.cpp @@ -4,9 +4,10 @@ #include "frc/livewindow/LiveWindow.h" +#include #include -#include #include +#include #include #include #include @@ -19,12 +20,15 @@ namespace { struct Component { bool firstTime = true; bool telemetryEnabled = false; + nt::StringPublisher namePub; + nt::StringPublisher typePub; }; struct Instance { Instance() { wpi::SendableRegistry::SetLiveWindowBuilderFactory( [] { return std::make_unique(); }); + enabledPub.Set(false); } wpi::mutex mutex; @@ -35,7 +39,8 @@ struct Instance { nt::NetworkTableInstance::GetDefault().GetTable("LiveWindow"); std::shared_ptr statusTable = liveWindowTable->GetSubTable(".status"); - nt::NetworkTableEntry enabledEntry = statusTable->GetEntry("LW Enabled"); + nt::BooleanPublisher enabledPub = + statusTable->GetBooleanTopic("LW Enabled").Publish(); bool startLiveWindow = false; bool liveWindowEnabled = false; @@ -139,7 +144,7 @@ void LiveWindow::SetEnabled(bool enabled) { inst.disabled(); } } - inst.enabledEntry.SetBoolean(enabled); + inst.enabledPub.Set(enabled); } void LiveWindow::UpdateValues() { @@ -186,10 +191,12 @@ void LiveWindow::UpdateValuesUnsafe() { } else { table = ssTable->GetSubTable(cbdata.name); } - table->GetEntry(".name").SetString(cbdata.name); + comp.namePub = nt::StringTopic{table->GetTopic(".name")}.Publish(); + comp.namePub.Set(cbdata.name); static_cast(cbdata.builder).SetTable(table); cbdata.sendable->InitSendable(cbdata.builder); - ssTable->GetEntry(".type").SetString("LW Subsystem"); + comp.typePub = nt::StringTopic{ssTable->GetTopic(".type")}.Publish(); + comp.typePub.Set("LW Subsystem"); comp.firstTime = false; } diff --git a/wpilibc/src/main/native/cpp/shuffleboard/RecordingController.cpp b/wpilibc/src/main/native/cpp/shuffleboard/RecordingController.cpp index 834c4be88d..86b6198df7 100644 --- a/wpilibc/src/main/native/cpp/shuffleboard/RecordingController.cpp +++ b/wpilibc/src/main/native/cpp/shuffleboard/RecordingController.cpp @@ -9,29 +9,30 @@ using namespace frc; using namespace frc::detail; -RecordingController::RecordingController(nt::NetworkTableInstance ntInstance) - : m_recordingControlEntry(), m_recordingFileNameFormatEntry() { +RecordingController::RecordingController(nt::NetworkTableInstance ntInstance) { m_recordingControlEntry = - ntInstance.GetEntry("/Shuffleboard/.recording/RecordData"); + ntInstance.GetBooleanTopic("/Shuffleboard/.recording/RecordData") + .Publish(); m_recordingFileNameFormatEntry = - ntInstance.GetEntry("/Shuffleboard/.recording/FileNameFormat"); + ntInstance.GetStringTopic("/Shuffleboard/.recording/FileNameFormat") + .Publish(); m_eventsTable = ntInstance.GetTable("/Shuffleboard/.recording/events"); } void RecordingController::StartRecording() { - m_recordingControlEntry.SetBoolean(true); + m_recordingControlEntry.Set(true); } void RecordingController::StopRecording() { - m_recordingControlEntry.SetBoolean(false); + m_recordingControlEntry.Set(false); } void RecordingController::SetRecordingFileNameFormat(std::string_view format) { - m_recordingFileNameFormatEntry.SetString(format); + m_recordingFileNameFormatEntry.Set(format); } void RecordingController::ClearRecordingFileNameFormat() { - m_recordingFileNameFormatEntry.Delete(); + m_recordingFileNameFormatEntry.Set(""); } void RecordingController::AddEventMarker( @@ -43,6 +44,6 @@ void RecordingController::AddEventMarker( return; } m_eventsTable->GetSubTable(name)->GetEntry("Info").SetStringArray( - {std::string{description}, - std::string{ShuffleboardEventImportanceName(importance)}}); + {{std::string{description}, + std::string{ShuffleboardEventImportanceName(importance)}}}); } diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardComponentBase.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardComponentBase.cpp index 940f6ab90e..c43fc90890 100644 --- a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardComponentBase.cpp +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardComponentBase.cpp @@ -22,27 +22,21 @@ void ShuffleboardComponentBase::BuildMetadata( return; } // Component type - if (GetType() == "") { - metaTable->GetEntry("PreferredComponent").Delete(); - } else { - metaTable->GetEntry("PreferredComponent").ForceSetString(GetType()); + if (!GetType().empty()) { + metaTable->GetEntry("PreferredComponent").SetString(GetType()); } // Tile size - if (m_width <= 0 || m_height <= 0) { - metaTable->GetEntry("Size").Delete(); - } else { + if (m_width > 0 && m_height > 0) { metaTable->GetEntry("Size").SetDoubleArray( - {static_cast(m_width), static_cast(m_height)}); + {{static_cast(m_width), static_cast(m_height)}}); } // Tile position - if (m_column < 0 || m_row < 0) { - metaTable->GetEntry("Position").Delete(); - } else { + if (m_column >= 0 && m_row >= 0) { metaTable->GetEntry("Position") .SetDoubleArray( - {static_cast(m_column), static_cast(m_row)}); + {{static_cast(m_column), static_cast(m_row)}}); } // Custom properties @@ -63,7 +57,7 @@ const std::string& ShuffleboardComponentBase::GetType() const { return m_type; } -const wpi::StringMap>& -ShuffleboardComponentBase::GetProperties() const { +const wpi::StringMap& ShuffleboardComponentBase::GetProperties() + const { return m_properties; } diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardContainer.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardContainer.cpp index 004f7c538a..e4446a3412 100644 --- a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardContainer.cpp +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardContainer.cpp @@ -4,6 +4,7 @@ #include "frc/shuffleboard/ShuffleboardContainer.h" +#include #include #include "frc/Errors.h" @@ -74,14 +75,15 @@ ComplexWidget& ShuffleboardContainer::Add(wpi::Sendable& sendable) { return Add(name, sendable); } -SimpleWidget& ShuffleboardContainer::Add( - std::string_view title, std::shared_ptr defaultValue) { +SimpleWidget& ShuffleboardContainer::Add(std::string_view title, + const nt::Value& defaultValue) { CheckTitle(title); auto widget = std::make_unique(*this, title); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); - ptr->GetEntry().SetDefaultValue(defaultValue); + ptr->GetEntry(nt::GetStringFromType(defaultValue.type())) + .SetDefault(defaultValue); return *ptr; } @@ -95,9 +97,14 @@ SimpleWidget& ShuffleboardContainer::Add(std::string_view title, return Add(title, nt::Value::MakeDouble(defaultValue)); } +SimpleWidget& ShuffleboardContainer::Add(std::string_view title, + float defaultValue) { + return Add(title, nt::Value::MakeFloat(defaultValue)); +} + SimpleWidget& ShuffleboardContainer::Add(std::string_view title, int defaultValue) { - return Add(title, nt::Value::MakeDouble(defaultValue)); + return Add(title, nt::Value::MakeInteger(defaultValue)); } SimpleWidget& ShuffleboardContainer::Add(std::string_view title, @@ -120,6 +127,16 @@ SimpleWidget& ShuffleboardContainer::Add(std::string_view title, return Add(title, nt::Value::MakeDoubleArray(defaultValue)); } +SimpleWidget& ShuffleboardContainer::Add(std::string_view title, + wpi::span defaultValue) { + return Add(title, nt::Value::MakeFloatArray(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::Add( + std::string_view title, wpi::span defaultValue) { + return Add(title, nt::Value::MakeIntegerArray(defaultValue)); +} + SimpleWidget& ShuffleboardContainer::Add( std::string_view title, wpi::span defaultValue) { return Add(title, nt::Value::MakeStringArray(defaultValue)); @@ -127,13 +144,13 @@ SimpleWidget& ShuffleboardContainer::Add( SuppliedValueWidget& ShuffleboardContainer::AddString( std::string_view title, std::function supplier) { - static auto setter = [](nt::NetworkTableEntry entry, std::string value) { + static auto setter = [](nt::GenericPublisher& entry, std::string value) { entry.SetString(value); }; CheckTitle(title); auto widget = std::make_unique>( - *this, title, supplier, setter); + *this, title, "string", supplier, setter); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); return *ptr; @@ -141,13 +158,46 @@ SuppliedValueWidget& ShuffleboardContainer::AddString( SuppliedValueWidget& ShuffleboardContainer::AddNumber( std::string_view title, std::function supplier) { - static auto setter = [](nt::NetworkTableEntry entry, double value) { + return AddDouble(title, std::move(supplier)); +} + +SuppliedValueWidget& ShuffleboardContainer::AddDouble( + std::string_view title, std::function supplier) { + static auto setter = [](nt::GenericPublisher& entry, double value) { entry.SetDouble(value); }; CheckTitle(title); - auto widget = std::make_unique>(*this, title, - supplier, setter); + auto widget = std::make_unique>( + *this, title, "double", supplier, setter); + auto ptr = widget.get(); + m_components.emplace_back(std::move(widget)); + return *ptr; +} + +SuppliedValueWidget& ShuffleboardContainer::AddFloat( + std::string_view title, std::function supplier) { + static auto setter = [](nt::GenericPublisher& entry, float value) { + entry.SetFloat(value); + }; + + CheckTitle(title); + auto widget = std::make_unique>( + *this, title, "float", supplier, setter); + auto ptr = widget.get(); + m_components.emplace_back(std::move(widget)); + return *ptr; +} + +SuppliedValueWidget& ShuffleboardContainer::AddInteger( + std::string_view title, std::function supplier) { + static auto setter = [](nt::GenericPublisher& entry, int64_t value) { + entry.SetInteger(value); + }; + + CheckTitle(title); + auto widget = std::make_unique>( + *this, title, "int", supplier, setter); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); return *ptr; @@ -155,13 +205,13 @@ SuppliedValueWidget& ShuffleboardContainer::AddNumber( SuppliedValueWidget& ShuffleboardContainer::AddBoolean( std::string_view title, std::function supplier) { - static auto setter = [](nt::NetworkTableEntry entry, bool value) { + static auto setter = [](nt::GenericPublisher& entry, bool value) { entry.SetBoolean(value); }; CheckTitle(title); - auto widget = std::make_unique>(*this, title, - supplier, setter); + auto widget = std::make_unique>( + *this, title, "boolean", supplier, setter); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); return *ptr; @@ -171,14 +221,14 @@ SuppliedValueWidget>& ShuffleboardContainer::AddStringArray( std::string_view title, std::function()> supplier) { - static auto setter = [](nt::NetworkTableEntry entry, + static auto setter = [](nt::GenericPublisher& entry, std::vector value) { entry.SetStringArray(value); }; CheckTitle(title); auto widget = std::make_unique>>( - *this, title, supplier, setter); + *this, title, "string[]", supplier, setter); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); return *ptr; @@ -186,14 +236,50 @@ ShuffleboardContainer::AddStringArray( SuppliedValueWidget>& ShuffleboardContainer::AddNumberArray( std::string_view title, std::function()> supplier) { - static auto setter = [](nt::NetworkTableEntry entry, + return AddDoubleArray(title, std::move(supplier)); +} + +SuppliedValueWidget>& ShuffleboardContainer::AddDoubleArray( + std::string_view title, std::function()> supplier) { + static auto setter = [](nt::GenericPublisher& entry, std::vector value) { entry.SetDoubleArray(value); }; CheckTitle(title); auto widget = std::make_unique>>( - *this, title, supplier, setter); + *this, title, "double[]", supplier, setter); + auto ptr = widget.get(); + m_components.emplace_back(std::move(widget)); + return *ptr; +} + +SuppliedValueWidget>& ShuffleboardContainer::AddFloatArray( + std::string_view title, std::function()> supplier) { + static auto setter = [](nt::GenericPublisher& entry, + std::vector value) { + entry.SetFloatArray(value); + }; + + CheckTitle(title); + auto widget = std::make_unique>>( + *this, title, "float[]", supplier, setter); + auto ptr = widget.get(); + m_components.emplace_back(std::move(widget)); + return *ptr; +} + +SuppliedValueWidget>& +ShuffleboardContainer::AddIntegerArray( + std::string_view title, std::function()> supplier) { + static auto setter = [](nt::GenericPublisher& entry, + std::vector value) { + entry.SetIntegerArray(value); + }; + + CheckTitle(title); + auto widget = std::make_unique>>( + *this, title, "int[]", supplier, setter); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); return *ptr; @@ -201,36 +287,43 @@ SuppliedValueWidget>& ShuffleboardContainer::AddNumberArray( SuppliedValueWidget>& ShuffleboardContainer::AddBooleanArray( std::string_view title, std::function()> supplier) { - static auto setter = [](nt::NetworkTableEntry entry, std::vector value) { + static auto setter = [](nt::GenericPublisher& entry, std::vector value) { entry.SetBooleanArray(value); }; CheckTitle(title); auto widget = std::make_unique>>( - *this, title, supplier, setter); + *this, title, "boolean[]", supplier, setter); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); return *ptr; } -SuppliedValueWidget& ShuffleboardContainer::AddRaw( - std::string_view title, std::function supplier) { - static auto setter = [](nt::NetworkTableEntry entry, std::string_view value) { - entry.SetRaw(value); - }; +SuppliedValueWidget>& ShuffleboardContainer::AddRaw( + std::string_view title, std::function()> supplier) { + return AddRaw(title, "raw", std::move(supplier)); +} + +SuppliedValueWidget>& ShuffleboardContainer::AddRaw( + std::string_view title, std::string_view typeString, + std::function()> supplier) { + static auto setter = [](nt::GenericPublisher& entry, + std::vector value) { entry.SetRaw(value); }; CheckTitle(title); - auto widget = std::make_unique>( - *this, title, supplier, setter); + auto widget = std::make_unique>>( + *this, title, typeString, supplier, setter); auto ptr = widget.get(); m_components.emplace_back(std::move(widget)); return *ptr; } SimpleWidget& ShuffleboardContainer::AddPersistent( - std::string_view title, std::shared_ptr defaultValue) { + std::string_view title, const nt::Value& defaultValue) { auto& widget = Add(title, defaultValue); - widget.GetEntry().SetPersistent(); + widget.GetEntry(nt::GetStringFromType(defaultValue.type())) + .GetTopic() + .SetPersistent(true); return widget; } @@ -244,9 +337,14 @@ SimpleWidget& ShuffleboardContainer::AddPersistent(std::string_view title, return AddPersistent(title, nt::Value::MakeDouble(defaultValue)); } +SimpleWidget& ShuffleboardContainer::AddPersistent(std::string_view title, + float defaultValue) { + return AddPersistent(title, nt::Value::MakeFloat(defaultValue)); +} + SimpleWidget& ShuffleboardContainer::AddPersistent(std::string_view title, int defaultValue) { - return AddPersistent(title, nt::Value::MakeDouble(defaultValue)); + return AddPersistent(title, nt::Value::MakeInteger(defaultValue)); } SimpleWidget& ShuffleboardContainer::AddPersistent( @@ -264,6 +362,16 @@ SimpleWidget& ShuffleboardContainer::AddPersistent( return AddPersistent(title, nt::Value::MakeDoubleArray(defaultValue)); } +SimpleWidget& ShuffleboardContainer::AddPersistent( + std::string_view title, wpi::span defaultValue) { + return AddPersistent(title, nt::Value::MakeFloatArray(defaultValue)); +} + +SimpleWidget& ShuffleboardContainer::AddPersistent( + std::string_view title, wpi::span defaultValue) { + return AddPersistent(title, nt::Value::MakeIntegerArray(defaultValue)); +} + SimpleWidget& ShuffleboardContainer::AddPersistent( std::string_view title, wpi::span defaultValue) { return AddPersistent(title, nt::Value::MakeStringArray(defaultValue)); diff --git a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp index 083b4a213c..a315b9072f 100644 --- a/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp +++ b/wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardInstance.cpp @@ -46,7 +46,7 @@ void ShuffleboardInstance::Update() { for (auto& entry : m_impl->tabs) { tabTitles.emplace_back(entry.second->GetTitle()); } - m_impl->rootMetaTable->GetEntry("Tabs").ForceSetStringArray(tabTitles); + m_impl->rootMetaTable->GetEntry("Tabs").SetStringArray(tabTitles); m_impl->tabsChanged = false; } for (auto& entry : m_impl->tabs) { @@ -75,9 +75,9 @@ void ShuffleboardInstance::DisableActuatorWidgets() { } void ShuffleboardInstance::SelectTab(int index) { - m_impl->rootMetaTable->GetEntry("Selected").ForceSetDouble(index); + m_impl->rootMetaTable->GetEntry("Selected").SetDouble(index); } void ShuffleboardInstance::SelectTab(std::string_view title) { - m_impl->rootMetaTable->GetEntry("Selected").ForceSetString(title); + m_impl->rootMetaTable->GetEntry("Selected").SetString(title); } diff --git a/wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp b/wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp index 390c9c4992..2305bd1353 100644 --- a/wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp +++ b/wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp @@ -14,18 +14,26 @@ SimpleWidget::SimpleWidget(ShuffleboardContainer& parent, std::string_view title) : ShuffleboardValue(title), ShuffleboardWidget(parent, title), m_entry() {} -nt::NetworkTableEntry SimpleWidget::GetEntry() { +nt::GenericEntry& SimpleWidget::GetEntry() { if (!m_entry) { ForceGenerate(); } return m_entry; } +nt::GenericEntry& SimpleWidget::GetEntry(std::string_view typeString) { + if (!m_entry) { + m_typeString = typeString; + ForceGenerate(); + } + return m_entry; +} + void SimpleWidget::BuildInto(std::shared_ptr parentTable, std::shared_ptr metaTable) { BuildMetadata(metaTable); if (!m_entry) { - m_entry = parentTable->GetEntry(GetTitle()); + m_entry = parentTable->GetTopic(GetTitle()).GetGenericEntry(m_typeString); } } diff --git a/wpilibc/src/main/native/cpp/smartdashboard/Field2d.cpp b/wpilibc/src/main/native/cpp/smartdashboard/Field2d.cpp index 2915a124fc..ec52e9ccee 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/Field2d.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/Field2d.cpp @@ -4,6 +4,7 @@ #include "frc/smartdashboard/Field2d.h" +#include #include #include @@ -57,7 +58,7 @@ FieldObject2d* Field2d::GetObject(std::string_view name) { std::make_unique(name, FieldObject2d::private_init{})); auto obj = m_objects.back().get(); if (m_table) { - obj->m_entry = m_table->GetEntry(obj->m_name); + obj->m_entry = m_table->GetDoubleArrayTopic(obj->m_name).GetEntry({}); } return obj; } @@ -74,7 +75,7 @@ void Field2d::InitSendable(nt::NTSendableBuilder& builder) { m_table = builder.GetTable(); for (auto&& obj : m_objects) { std::scoped_lock lock2(obj->m_mutex); - obj->m_entry = m_table->GetEntry(obj->m_name); + obj->m_entry = m_table->GetDoubleArrayTopic(obj->m_name).GetEntry({}); obj->UpdateEntry(true); } } diff --git a/wpilibc/src/main/native/cpp/smartdashboard/FieldObject2d.cpp b/wpilibc/src/main/native/cpp/smartdashboard/FieldObject2d.cpp index b218272ad6..9d334e46ba 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/FieldObject2d.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/FieldObject2d.cpp @@ -6,9 +6,6 @@ #include -#include -#include - #include "frc/trajectory/Trajectory.h" using namespace frc; @@ -83,41 +80,17 @@ void FieldObject2d::UpdateEntry(bool setDefault) { if (!m_entry) { return; } - if (m_poses.size() < (255 / 3)) { - wpi::SmallVector arr; - for (auto&& pose : m_poses) { - auto& translation = pose.Translation(); - arr.push_back(translation.X().value()); - arr.push_back(translation.Y().value()); - arr.push_back(pose.Rotation().Degrees().value()); - } - if (setDefault) { - m_entry.SetDefaultDoubleArray(arr); - } else { - m_entry.ForceSetDoubleArray(arr); - } + wpi::SmallVector arr; + for (auto&& pose : m_poses) { + auto& translation = pose.Translation(); + arr.push_back(translation.X().value()); + arr.push_back(translation.Y().value()); + arr.push_back(pose.Rotation().Degrees().value()); + } + if (setDefault) { + m_entry.SetDefault(arr); } else { - // send as raw array of doubles if too big for NT array - std::vector arr; - arr.resize(m_poses.size() * 3 * 8); - char* p = arr.data(); - for (auto&& pose : m_poses) { - auto& translation = pose.Translation(); - wpi::support::endian::write64be( - p, wpi::DoubleToBits(translation.X().value())); - p += 8; - wpi::support::endian::write64be( - p, wpi::DoubleToBits(translation.Y().value())); - p += 8; - wpi::support::endian::write64be( - p, wpi::DoubleToBits(pose.Rotation().Degrees().value())); - p += 8; - } - if (setDefault) { - m_entry.SetDefaultRaw({arr.data(), arr.size()}); - } else { - m_entry.ForceSetRaw({arr.data(), arr.size()}); - } + m_entry.Set(arr); } } @@ -125,46 +98,15 @@ void FieldObject2d::UpdateFromEntry() const { if (!m_entry) { return; } - auto val = m_entry.GetValue(); - if (!val) { + auto arr = m_entry.Get(); + auto size = arr.size(); + if ((size % 3) != 0) { return; } - - if (val->IsDoubleArray()) { - auto arr = val->GetDoubleArray(); - auto size = arr.size(); - if ((size % 3) != 0) { - return; - } - m_poses.resize(size / 3); - for (size_t i = 0; i < size / 3; ++i) { - m_poses[i] = - Pose2d{units::meter_t{arr[i * 3 + 0]}, units::meter_t{arr[i * 3 + 1]}, - units::degree_t{arr[i * 3 + 2]}}; - } - } else if (val->IsRaw()) { - // treat it simply as an array of doubles - std::string_view data = val->GetRaw(); - - // must be triples of doubles - auto size = data.size(); - if ((size % (3 * 8)) != 0) { - return; - } - m_poses.resize(size / (3 * 8)); - const char* p = data.data(); - for (size_t i = 0; i < size / (3 * 8); ++i) { - double x = wpi::BitsToDouble( - wpi::support::endian::readNext(p)); - double y = wpi::BitsToDouble( - wpi::support::endian::readNext(p)); - double rot = wpi::BitsToDouble( - wpi::support::endian::readNext(p)); - m_poses[i] = - Pose2d{units::meter_t{x}, units::meter_t{y}, units::degree_t{rot}}; - } + m_poses.resize(size / 3); + for (size_t i = 0; i < size / 3; ++i) { + m_poses[i] = + Pose2d{units::meter_t{arr[i * 3 + 0]}, units::meter_t{arr[i * 3 + 1]}, + units::degree_t{arr[i * 3 + 2]}}; } } diff --git a/wpilibc/src/main/native/cpp/smartdashboard/Mechanism2d.cpp b/wpilibc/src/main/native/cpp/smartdashboard/Mechanism2d.cpp index 61a71325b2..355cbc6ef0 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/Mechanism2d.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/Mechanism2d.cpp @@ -5,13 +5,14 @@ #include "frc/smartdashboard/Mechanism2d.h" #include +#include #include using namespace frc; -static constexpr char kBackgroundColor[] = "backgroundColor"; -static constexpr char kDims[] = "dims"; +static constexpr std::string_view kBackgroundColor = "backgroundColor"; +static constexpr std::string_view kDims = "dims"; Mechanism2d::Mechanism2d(double width, double height, const Color8Bit& backgroundColor) @@ -35,8 +36,8 @@ MechanismRoot2d* Mechanism2d::GetRoot(std::string_view name, double x, void Mechanism2d::SetBackgroundColor(const Color8Bit& color) { m_color = color.HexString(); - if (m_table) { - m_table->GetEntry(kBackgroundColor).SetString(m_color); + if (m_colorPub) { + m_colorPub.Set(m_color); } } @@ -45,8 +46,10 @@ void Mechanism2d::InitSendable(nt::NTSendableBuilder& builder) { std::scoped_lock lock(m_mutex); m_table = builder.GetTable(); - m_table->GetEntry(kDims).SetDoubleArray({m_width, m_height}); - m_table->GetEntry(kBackgroundColor).SetString(m_color); + m_dimsPub = m_table->GetDoubleArrayTopic(kDims).Publish(); + m_dimsPub.Set({{m_width, m_height}}); + m_colorPub = m_table->GetStringTopic(kBackgroundColor).Publish(); + m_colorPub.Set(m_color); for (const auto& entry : m_roots) { const auto& root = entry.getValue().get(); root->Update(m_table->GetSubTable(entry.getKey())); diff --git a/wpilibc/src/main/native/cpp/smartdashboard/MechanismLigament2d.cpp b/wpilibc/src/main/native/cpp/smartdashboard/MechanismLigament2d.cpp index bbe2fb3473..a43aa16f5d 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/MechanismLigament2d.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/MechanismLigament2d.cpp @@ -21,38 +21,48 @@ MechanismLigament2d::MechanismLigament2d(std::string_view name, double length, void MechanismLigament2d::UpdateEntries( std::shared_ptr table) { - table->GetEntry(".type").SetString("line"); + m_typePub = table->GetStringTopic(".type").Publish(); + m_typePub.Set("line"); - m_colorEntry = table->GetEntry("color"); - m_angleEntry = table->GetEntry("angle"); - m_weightEntry = table->GetEntry("weight"); - m_lengthEntry = table->GetEntry("length"); - Flush(); + m_colorEntry = table->GetStringTopic("color").GetEntry(""); + m_colorEntry.Set(m_color); + m_angleEntry = table->GetDoubleTopic("angle").GetEntry(0.0); + m_angleEntry.Set(m_angle); + m_weightEntry = table->GetDoubleTopic("weight").GetEntry(0.0); + m_weightEntry.Set(m_weight); + m_lengthEntry = table->GetDoubleTopic("length").GetEntry(0.0); + m_lengthEntry.Set(m_length); } void MechanismLigament2d::SetColor(const Color8Bit& color) { std::scoped_lock lock(m_mutex); std::snprintf(m_color, sizeof(m_color), "#%02X%02X%02X", color.red, color.green, color.blue); - Flush(); + if (m_colorEntry) { + m_colorEntry.Set(m_color); + } } void MechanismLigament2d::SetAngle(units::degree_t angle) { std::scoped_lock lock(m_mutex); m_angle = angle.value(); - Flush(); + if (m_angleEntry) { + m_angleEntry.Set(m_angle); + } } void MechanismLigament2d::SetLineWeight(double lineWidth) { std::scoped_lock lock(m_mutex); m_weight = lineWidth; - Flush(); + if (m_weightEntry) { + m_weightEntry.Set(m_weight); + } } Color8Bit MechanismLigament2d::GetColor() { std::scoped_lock lock(m_mutex); if (m_colorEntry) { - auto color = m_colorEntry.GetString(""); + auto color = m_colorEntry.Get(); std::strncpy(m_color, color.c_str(), sizeof(m_color)); m_color[sizeof(m_color) - 1] = '\0'; } @@ -64,7 +74,7 @@ Color8Bit MechanismLigament2d::GetColor() { double MechanismLigament2d::GetAngle() { std::scoped_lock lock(m_mutex); if (m_angleEntry) { - m_angle = m_angleEntry.GetDouble(0.0); + m_angle = m_angleEntry.Get(); } return m_angle; } @@ -72,7 +82,7 @@ double MechanismLigament2d::GetAngle() { double MechanismLigament2d::GetLength() { std::scoped_lock lock(m_mutex); if (m_lengthEntry) { - m_length = m_lengthEntry.GetDouble(0.0); + m_length = m_lengthEntry.Get(); } return m_length; } @@ -80,7 +90,7 @@ double MechanismLigament2d::GetLength() { double MechanismLigament2d::GetLineWeight() { std::scoped_lock lock(m_mutex); if (m_weightEntry) { - m_weight = m_weightEntry.GetDouble(0.0); + m_weight = m_weightEntry.Get(); } return m_weight; } @@ -88,16 +98,7 @@ double MechanismLigament2d::GetLineWeight() { void MechanismLigament2d::SetLength(double length) { std::scoped_lock lock(m_mutex); m_length = length; - Flush(); -} - -#define SAFE_WRITE(data, Type) \ - if (m_##data##Entry) { \ - m_##data##Entry.Set##Type(m_##data); \ + if (m_lengthEntry) { + m_lengthEntry.Set(length); } -void MechanismLigament2d::Flush() { - SAFE_WRITE(color, String) - SAFE_WRITE(angle, Double) - SAFE_WRITE(length, Double) - SAFE_WRITE(weight, Double) } diff --git a/wpilibc/src/main/native/cpp/smartdashboard/MechanismRoot2d.cpp b/wpilibc/src/main/native/cpp/smartdashboard/MechanismRoot2d.cpp index 44fd231c31..a40f5ee661 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/MechanismRoot2d.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/MechanismRoot2d.cpp @@ -20,16 +20,16 @@ void MechanismRoot2d::SetPosition(double x, double y) { } void MechanismRoot2d::UpdateEntries(std::shared_ptr table) { - m_xEntry = table->GetEntry("x"); - m_yEntry = table->GetEntry("y"); + m_xPub = table->GetDoubleTopic("x").Publish(); + m_yPub = table->GetDoubleTopic("y").Publish(); Flush(); } inline void MechanismRoot2d::Flush() { - if (m_xEntry) { - m_xEntry.SetDouble(m_x); + if (m_xPub) { + m_xPub.Set(m_x); } - if (m_yEntry) { - m_yEntry.SetDouble(m_y); + if (m_yPub) { + m_yPub.Set(m_y); } } diff --git a/wpilibc/src/main/native/cpp/smartdashboard/SendableBuilderImpl.cpp b/wpilibc/src/main/native/cpp/smartdashboard/SendableBuilderImpl.cpp index 48af1988a7..9c35e4f238 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/SendableBuilderImpl.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/SendableBuilderImpl.cpp @@ -4,16 +4,37 @@ #include "frc/smartdashboard/SendableBuilderImpl.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include #include "frc/smartdashboard/SmartDashboard.h" using namespace frc; +template +void SendableBuilderImpl::PropertyImpl::Update(bool controllable, + int64_t time) { + if (controllable && sub && updateLocal) { + updateLocal(sub); + } else if (pub && updateNetwork) { + updateNetwork(pub, time); + } +} + void SendableBuilderImpl::SetTable(std::shared_ptr table) { m_table = table; - m_controllableEntry = table->GetEntry(".controllable"); + m_controllablePublisher = table->GetBooleanTopic(".controllable").Publish(); + m_controllablePublisher.SetDefault(false); } std::shared_ptr SendableBuilderImpl::GetTable() { @@ -31,9 +52,7 @@ bool SendableBuilderImpl::IsActuator() const { void SendableBuilderImpl::Update() { uint64_t time = nt::Now(); for (auto& property : m_properties) { - if (property.update) { - property.update(property.entry, time); - } + property->Update(m_controllable, time); } for (auto& updateTable : m_updateTables) { updateTable(); @@ -41,20 +60,16 @@ void SendableBuilderImpl::Update() { } void SendableBuilderImpl::StartListeners() { - for (auto& property : m_properties) { - property.StartListener(); - } - if (m_controllableEntry) { - m_controllableEntry.SetBoolean(true); + m_controllable = true; + if (m_controllablePublisher) { + m_controllablePublisher.Set(true); } } void SendableBuilderImpl::StopListeners() { - for (auto& property : m_properties) { - property.StopListener(); - } - if (m_controllableEntry) { - m_controllableEntry.SetBoolean(false); + m_controllable = false; + if (m_controllablePublisher) { + m_controllablePublisher.Set(false); } } @@ -77,11 +92,17 @@ void SendableBuilderImpl::ClearProperties() { } void SendableBuilderImpl::SetSmartDashboardType(std::string_view type) { - m_table->GetEntry(".type").SetString(type); + if (!m_typePublisher) { + m_typePublisher = m_table->GetStringTopic(".type").Publish(); + } + m_typePublisher.Set(type); } void SendableBuilderImpl::SetActuator(bool value) { - m_table->GetEntry(".actuator").SetBoolean(value); + if (!m_actuatorPublisher) { + m_actuatorPublisher = m_table->GetBooleanTopic(".actuator").Publish(); + } + m_actuatorPublisher.Set(value); m_actuator = value; } @@ -89,272 +110,183 @@ void SendableBuilderImpl::SetSafeState(std::function func) { m_safeState = func; } -void SendableBuilderImpl::SetUpdateTable(std::function func) { +void SendableBuilderImpl::SetUpdateTable(wpi::unique_function func) { m_updateTables.emplace_back(std::move(func)); } -nt::NetworkTableEntry SendableBuilderImpl::GetEntry(std::string_view key) { - return m_table->GetEntry(key); +nt::Topic SendableBuilderImpl::GetTopic(std::string_view key) { + return m_table->GetTopic(key); +} + +template +void SendableBuilderImpl::AddPropertyImpl(Topic topic, Getter getter, + Setter setter) { + auto prop = std::make_unique>(); + if (getter) { + prop->pub = topic.Publish(); + prop->updateNetwork = [=](auto& pub, int64_t time) { + pub.Set(getter(), time); + }; + } + if (setter) { + prop->sub = topic.Subscribe({}); + prop->updateLocal = [=](auto& sub) { + for (auto&& val : sub.ReadQueue()) { + setter(val.value); + } + }; + } + m_properties.emplace_back(std::move(prop)); } void SendableBuilderImpl::AddBooleanProperty(std::string_view key, std::function getter, std::function setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(nt::Value::MakeBoolean(getter(), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsBoolean()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetBoolean()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddPropertyImpl(m_table->GetBooleanTopic(key), std::move(getter), + std::move(setter)); +} + +void SendableBuilderImpl::AddIntegerProperty( + std::string_view key, std::function getter, + std::function setter) { + AddPropertyImpl(m_table->GetIntegerTopic(key), std::move(getter), + std::move(setter)); +} + +void SendableBuilderImpl::AddFloatProperty(std::string_view key, + std::function getter, + std::function setter) { + AddPropertyImpl(m_table->GetFloatTopic(key), std::move(getter), + std::move(setter)); } void SendableBuilderImpl::AddDoubleProperty( std::string_view key, std::function getter, std::function setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(nt::Value::MakeDouble(getter(), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsDouble()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetDouble()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddPropertyImpl(m_table->GetDoubleTopic(key), std::move(getter), + std::move(setter)); } void SendableBuilderImpl::AddStringProperty( std::string_view key, std::function getter, std::function setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(nt::Value::MakeString(getter(), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsString()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetString()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddPropertyImpl(m_table->GetStringTopic(key), std::move(getter), + std::move(setter)); } void SendableBuilderImpl::AddBooleanArrayProperty( std::string_view key, std::function()> getter, std::function)> setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(nt::Value::MakeBooleanArray(getter(), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsBooleanArray()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetBooleanArray()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddPropertyImpl(m_table->GetBooleanArrayTopic(key), std::move(getter), + std::move(setter)); +} + +void SendableBuilderImpl::AddIntegerArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) { + AddPropertyImpl(m_table->GetIntegerArrayTopic(key), std::move(getter), + std::move(setter)); +} + +void SendableBuilderImpl::AddFloatArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) { + AddPropertyImpl(m_table->GetFloatArrayTopic(key), std::move(getter), + std::move(setter)); } void SendableBuilderImpl::AddDoubleArrayProperty( std::string_view key, std::function()> getter, std::function)> setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(nt::Value::MakeDoubleArray(getter(), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsDoubleArray()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetDoubleArray()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddPropertyImpl(m_table->GetDoubleArrayTopic(key), std::move(getter), + std::move(setter)); } void SendableBuilderImpl::AddStringArrayProperty( std::string_view key, std::function()> getter, std::function)> setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(nt::Value::MakeStringArray(getter(), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsStringArray()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetStringArray()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddPropertyImpl(m_table->GetStringArrayTopic(key), std::move(getter), + std::move(setter)); } void SendableBuilderImpl::AddRawProperty( - std::string_view key, std::function getter, - std::function setter) { - m_properties.emplace_back(*m_table, key); + std::string_view key, std::string_view typeString, + std::function()> getter, + std::function)> setter) { + auto topic = m_table->GetRawTopic(key); + auto prop = std::make_unique>(); if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(nt::Value::MakeRaw(getter(), time)); + prop->pub = topic.Publish(typeString); + prop->updateNetwork = [=](auto& pub, int64_t time) { + pub.Set(getter(), time); }; } if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsRaw()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetRaw()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); + prop->sub = topic.Subscribe(typeString, {}); + prop->updateLocal = [=](auto& sub) { + for (auto&& val : sub.ReadQueue()) { + setter(val.value); + } }; } + m_properties.emplace_back(std::move(prop)); } -void SendableBuilderImpl::AddValueProperty( - std::string_view key, std::function()> getter, - std::function)> setter) { - m_properties.emplace_back(*m_table, key); +template +void SendableBuilderImpl::AddSmallPropertyImpl(Topic topic, Getter getter, + Setter setter) { + auto prop = std::make_unique>(); if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - entry.SetValue(getter()); + prop->pub = topic.Publish(); + prop->updateNetwork = [=](auto& pub, int64_t time) { + wpi::SmallVector buf; + pub.Set(getter(buf), time); }; } if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - SmartDashboard::PostListenerTask([=] { setter(event.value); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); + prop->sub = topic.Subscribe({}); + prop->updateLocal = [=](auto& sub) { + for (auto&& val : sub.ReadQueue()) { + setter(val.value); + } }; } + m_properties.emplace_back(std::move(prop)); } void SendableBuilderImpl::AddSmallStringProperty( std::string_view key, std::function& buf)> getter, std::function setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - wpi::SmallString<128> buf; - entry.SetValue(nt::Value::MakeString(getter(buf), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsString()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetString()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddSmallPropertyImpl(m_table->GetStringTopic(key), + std::move(getter), std::move(setter)); } void SendableBuilderImpl::AddSmallBooleanArrayProperty( std::string_view key, std::function(wpi::SmallVectorImpl& buf)> getter, std::function)> setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - wpi::SmallVector buf; - entry.SetValue(nt::Value::MakeBooleanArray(getter(buf), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsBooleanArray()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetBooleanArray()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddSmallPropertyImpl(m_table->GetBooleanArrayTopic(key), + std::move(getter), std::move(setter)); +} + +void SendableBuilderImpl::AddSmallIntegerArrayProperty( + std::string_view key, + std::function(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) { + AddSmallPropertyImpl(m_table->GetIntegerArrayTopic(key), + std::move(getter), std::move(setter)); +} + +void SendableBuilderImpl::AddSmallFloatArrayProperty( + std::string_view key, + std::function(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) { + AddSmallPropertyImpl(m_table->GetFloatArrayTopic(key), + std::move(getter), std::move(setter)); } void SendableBuilderImpl::AddSmallDoubleArrayProperty( @@ -362,28 +294,8 @@ void SendableBuilderImpl::AddSmallDoubleArrayProperty( std::function(wpi::SmallVectorImpl& buf)> getter, std::function)> setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - wpi::SmallVector buf; - entry.SetValue(nt::Value::MakeDoubleArray(getter(buf), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsDoubleArray()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetDoubleArray()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddSmallPropertyImpl(m_table->GetDoubleArrayTopic(key), + std::move(getter), std::move(setter)); } void SendableBuilderImpl::AddSmallStringArrayProperty( @@ -392,54 +304,31 @@ void SendableBuilderImpl::AddSmallStringArrayProperty( wpi::span(wpi::SmallVectorImpl& buf)> getter, std::function)> setter) { - m_properties.emplace_back(*m_table, key); - if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - wpi::SmallVector buf; - entry.SetValue(nt::Value::MakeStringArray(getter(buf), time)); - }; - } - if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsStringArray()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetStringArray()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); - }; - } + AddSmallPropertyImpl(m_table->GetStringArrayTopic(key), + std::move(getter), std::move(setter)); } void SendableBuilderImpl::AddSmallRawProperty( - std::string_view key, - std::function& buf)> getter, - std::function setter) { - m_properties.emplace_back(*m_table, key); + std::string_view key, std::string_view typeString, + std::function(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) { + auto topic = m_table->GetRawTopic(key); + auto prop = std::make_unique>(); if (getter) { - m_properties.back().update = [=](nt::NetworkTableEntry entry, - uint64_t time) { - wpi::SmallVector buf; - entry.SetValue(nt::Value::MakeRaw(getter(buf), time)); + prop->pub = topic.Publish(typeString); + prop->updateNetwork = [=](auto& pub, int64_t time) { + wpi::SmallVector buf; + pub.Set(getter(buf), time); }; } if (setter) { - m_properties.back().createListener = - [=](nt::NetworkTableEntry entry) -> NT_EntryListener { - return entry.AddListener( - [=](const nt::EntryNotification& event) { - if (!event.value->IsRaw()) { - return; - } - SmartDashboard::PostListenerTask( - [=] { setter(event.value->GetRaw()); }); - }, - NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE); + prop->sub = topic.Subscribe(typeString, {}); + prop->updateLocal = [=](auto& sub) { + for (auto&& val : sub.ReadQueue()) { + setter(val.value); + } }; } + m_properties.emplace_back(std::move(prop)); } diff --git a/wpilibc/src/main/native/cpp/smartdashboard/SendableChooserBase.cpp b/wpilibc/src/main/native/cpp/smartdashboard/SendableChooserBase.cpp index 550d40c3f4..9fa5b69428 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/SendableChooserBase.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/SendableChooserBase.cpp @@ -19,7 +19,8 @@ SendableChooserBase::SendableChooserBase(SendableChooserBase&& oth) m_defaultChoice(std::move(oth.m_defaultChoice)), m_selected(std::move(oth.m_selected)), m_haveSelected(std::move(oth.m_haveSelected)), - m_activeEntries(std::move(oth.m_activeEntries)), + m_instancePubs(std::move(oth.m_instancePubs)), + m_activePubs(std::move(oth.m_activePubs)), m_instance(std::move(oth.m_instance)) {} SendableChooserBase& SendableChooserBase::operator=(SendableChooserBase&& oth) { @@ -28,7 +29,8 @@ SendableChooserBase& SendableChooserBase::operator=(SendableChooserBase&& oth) { m_defaultChoice = std::move(oth.m_defaultChoice); m_selected = std::move(oth.m_selected); m_haveSelected = std::move(oth.m_haveSelected); - m_activeEntries = std::move(oth.m_activeEntries); + m_instancePubs = std::move(oth.m_instancePubs); + m_activePubs = std::move(oth.m_activePubs); m_instance = std::move(oth.m_instance); return *this; } diff --git a/wpilibc/src/main/native/cpp/smartdashboard/SmartDashboard.cpp b/wpilibc/src/main/native/cpp/smartdashboard/SmartDashboard.cpp index 508839440e..334a139aa5 100644 --- a/wpilibc/src/main/native/cpp/smartdashboard/SmartDashboard.cpp +++ b/wpilibc/src/main/native/cpp/smartdashboard/SmartDashboard.cpp @@ -70,22 +70,6 @@ bool SmartDashboard::IsPersistent(std::string_view key) { return GetInstance().table->GetEntry(key).IsPersistent(); } -void SmartDashboard::SetFlags(std::string_view key, unsigned int flags) { - GetInstance().table->GetEntry(key).SetFlags(flags); -} - -void SmartDashboard::ClearFlags(std::string_view key, unsigned int flags) { - GetInstance().table->GetEntry(key).ClearFlags(flags); -} - -unsigned int SmartDashboard::GetFlags(std::string_view key) { - return GetInstance().table->GetEntry(key).GetFlags(); -} - -void SmartDashboard::Delete(std::string_view key) { - GetInstance().table->Delete(key); -} - nt::NetworkTableEntry SmartDashboard::GetEntry(std::string_view key) { return GetInstance().table->GetEntry(key); } @@ -218,31 +202,32 @@ std::vector SmartDashboard::GetStringArray( return GetInstance().table->GetEntry(key).GetStringArray(defaultValue); } -bool SmartDashboard::PutRaw(std::string_view key, std::string_view value) { +bool SmartDashboard::PutRaw(std::string_view key, + wpi::span value) { return GetInstance().table->GetEntry(key).SetRaw(value); } bool SmartDashboard::SetDefaultRaw(std::string_view key, - std::string_view defaultValue) { + wpi::span defaultValue) { return GetInstance().table->GetEntry(key).SetDefaultRaw(defaultValue); } -std::string SmartDashboard::GetRaw(std::string_view key, - std::string_view defaultValue) { +std::vector SmartDashboard::GetRaw( + std::string_view key, wpi::span defaultValue) { return GetInstance().table->GetEntry(key).GetRaw(defaultValue); } bool SmartDashboard::PutValue(std::string_view keyName, - std::shared_ptr value) { + const nt::Value& value) { return GetInstance().table->GetEntry(keyName).SetValue(value); } bool SmartDashboard::SetDefaultValue(std::string_view key, - std::shared_ptr defaultValue) { + const nt::Value& defaultValue) { return GetInstance().table->GetEntry(key).SetDefaultValue(defaultValue); } -std::shared_ptr SmartDashboard::GetValue(std::string_view keyName) { +nt::Value SmartDashboard::GetValue(std::string_view keyName) { return GetInstance().table->GetEntry(keyName).GetValue(); } diff --git a/wpilibc/src/main/native/cppcs/RobotBase.cpp b/wpilibc/src/main/native/cppcs/RobotBase.cpp index 68a85b7ca3..1f018a1ca8 100644 --- a/wpilibc/src/main/native/cppcs/RobotBase.cpp +++ b/wpilibc/src/main/native/cppcs/RobotBase.cpp @@ -224,12 +224,25 @@ RobotBase::RobotBase() { auto inst = nt::NetworkTableInstance::GetDefault(); inst.SetNetworkIdentity("Robot"); + // subscribe to "" to force persistent values to progagate to local + nt::SubscribeMultiple(inst.GetHandle(), {{std::string_view{}}}); #ifdef __FRC_ROBORIO__ - inst.StartServer("/home/lvuser/networktables.ini"); + inst.StartServer("/home/lvuser/networktables.json"); #else inst.StartServer(); #endif + // wait for the NT server to actually start + int count = 0; + while ((inst.GetNetworkMode() & NT_NET_MODE_STARTING) != 0) { + using namespace std::chrono_literals; + std::this_thread::sleep_for(10ms); + ++count; + if (count > 100) { + fmt::print(stderr, "timed out while waiting for NT server to start\n"); + } + } + SmartDashboard::init(); if (IsReal()) { @@ -247,10 +260,5 @@ RobotBase::RobotBase() { DriverStation::InDisabled(true); // First and one-time initialization - inst.GetTable("LiveWindow") - ->GetSubTable(".status") - ->GetEntry("LW Enabled") - .SetBoolean(false); - LiveWindow::SetEnabled(false); } diff --git a/wpilibc/src/main/native/include/frc/IterativeRobotBase.h b/wpilibc/src/main/native/include/frc/IterativeRobotBase.h index 669f0cd8e4..7392e673ce 100644 --- a/wpilibc/src/main/native/include/frc/IterativeRobotBase.h +++ b/wpilibc/src/main/native/include/frc/IterativeRobotBase.h @@ -195,7 +195,7 @@ class IterativeRobotBase : public RobotBase { /** * Enables or disables flushing NetworkTables every loop iteration. - * By default, this is disabled. + * By default, this is enabled. * * @param enabled True to enable, false to disable */ @@ -227,7 +227,7 @@ class IterativeRobotBase : public RobotBase { Mode m_lastMode = Mode::kNone; units::second_t m_period; Watchdog m_watchdog; - bool m_ntFlushEnabled = false; + bool m_ntFlushEnabled = true; void PrintLoopOverrunMessage(); }; diff --git a/wpilibc/src/main/native/include/frc/event/NetworkBooleanEvent.h b/wpilibc/src/main/native/include/frc/event/NetworkBooleanEvent.h new file mode 100644 index 0000000000..85f7039175 --- /dev/null +++ b/wpilibc/src/main/native/include/frc/event/NetworkBooleanEvent.h @@ -0,0 +1,79 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include + +#include "BooleanEvent.h" + +namespace nt { +class BooleanSubscriber; +class BooleanTopic; +class NetworkTable; +class NetworkTableInstance; +} // namespace nt + +namespace frc { +/** + * A Button that uses a NetworkTable boolean field. + * + * This class is provided by the NewCommands VendorDep + */ +class NetworkBooleanEvent : public BooleanEvent { + public: + /** + * Creates a new event with the given boolean topic determining whether it is + * active. + * + * @param loop the loop that polls this event + * @param topic The boolean topic that contains the value + */ + NetworkBooleanEvent(EventLoop* loop, nt::BooleanTopic topic); + + /** + * Creates a new event with the given boolean subscriber determining whether + * it is active. + * + * @param loop the loop that polls this event + * @param sub The boolean subscriber that provides the value + */ + NetworkBooleanEvent(EventLoop* loop, nt::BooleanSubscriber sub); + + /** + * Creates a new event with the given boolean topic determining whether it is + * active. + * + * @param loop the loop that polls this event + * @param table The NetworkTable that contains the topic + * @param topicName The topic name within the table that contains the value + */ + NetworkBooleanEvent(EventLoop* loop, std::shared_ptr table, + std::string_view topicName); + + /** + * Creates a new event with the given boolean topic determining whether it is + * active. + * + * @param loop the loop that polls this event + * @param tableName The NetworkTable name that contains the topic + * @param topicName The topic name within the table that contains the value + */ + NetworkBooleanEvent(EventLoop* loop, std::string_view tableName, + std::string_view topicName); + + /** + * Creates a new event with the given boolean topic determining whether it is + * active. + * + * @param loop the loop that polls this event + * @param inst The NetworkTable instance to use + * @param tableName The NetworkTable that contains the topic + * @param topicName The topic name within the table that contains the value + */ + NetworkBooleanEvent(EventLoop* loop, nt::NetworkTableInstance inst, + std::string_view tableName, std::string_view topicName); +}; +} // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/RecordingController.h b/wpilibc/src/main/native/include/frc/shuffleboard/RecordingController.h index 83b23a424b..3ef5120876 100644 --- a/wpilibc/src/main/native/include/frc/shuffleboard/RecordingController.h +++ b/wpilibc/src/main/native/include/frc/shuffleboard/RecordingController.h @@ -8,8 +8,10 @@ #include #include +#include #include #include +#include #include #include "frc/shuffleboard/ShuffleboardEventImportance.h" @@ -30,8 +32,8 @@ class RecordingController final { ShuffleboardEventImportance importance); private: - nt::NetworkTableEntry m_recordingControlEntry; - nt::NetworkTableEntry m_recordingFileNameFormatEntry; + nt::BooleanPublisher m_recordingControlEntry; + nt::StringPublisher m_recordingFileNameFormatEntry; std::shared_ptr m_eventsTable; }; diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/SendableCameraWrapper.h b/wpilibc/src/main/native/include/frc/shuffleboard/SendableCameraWrapper.h index 4792ce3070..55701c2836 100644 --- a/wpilibc/src/main/native/include/frc/shuffleboard/SendableCameraWrapper.h +++ b/wpilibc/src/main/native/include/frc/shuffleboard/SendableCameraWrapper.h @@ -22,6 +22,7 @@ using CS_Handle = int; // NOLINT using CS_Source = CS_Handle; // NOLINT #endif +#include #include #include #include @@ -55,6 +56,17 @@ class SendableCameraWrapper */ SendableCameraWrapper(std::string_view cameraName, const private_init&); + /** + * Creates a new sendable wrapper. Private constructor to avoid direct + * instantiation with multiple wrappers floating around for the same camera. + * + * @param cameraName the name of the camera to wrap + * @param cameraUrls camera URLs + */ + SendableCameraWrapper(std::string_view cameraName, + wpi::span cameraUrls, + const private_init&); + /** * Gets a sendable wrapper object for the given video source, creating the * wrapper if one does not already exist for the source. @@ -73,6 +85,7 @@ class SendableCameraWrapper private: std::string m_uri; + nt::StringArrayPublisher m_streams; }; #ifndef DYNAMIC_CAMERA_SERVER @@ -83,6 +96,17 @@ inline SendableCameraWrapper::SendableCameraWrapper(std::string_view name, m_uri += name; } +inline SendableCameraWrapper::SendableCameraWrapper( + std::string_view cameraName, wpi::span cameraUrls, + const private_init&) + : SendableCameraWrapper(cameraName, private_init{}) { + m_streams = nt::NetworkTableInstance::GetDefault() + .GetStringArrayTopic( + fmt::format("/CameraPublisher/{}/streams", cameraName)) + .Publish(); + m_streams.Set(cameraUrls); +} + inline SendableCameraWrapper& SendableCameraWrapper::Wrap( const cs::VideoSource& source) { return Wrap(source.GetHandle()); @@ -102,12 +126,9 @@ inline SendableCameraWrapper& SendableCameraWrapper::Wrap( std::string_view cameraName, wpi::span cameraUrls) { auto& wrapper = detail::GetSendableCameraWrapper(cameraName); if (!wrapper) { - wrapper = - std::make_shared(cameraName, private_init{}); + wrapper = std::make_shared(cameraName, cameraUrls, + private_init{}); } - auto streams = fmt::format("/CameraPublisher/{}/streams", cameraName); - nt::NetworkTableInstance::GetDefault().GetEntry(streams).SetStringArray( - cameraUrls); return *wrapper; } #endif diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h index d33a2346f0..de27b3528a 100644 --- a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardComponentBase.h @@ -35,7 +35,7 @@ class ShuffleboardComponentBase : public virtual ShuffleboardValue { const std::string& GetType() const; protected: - wpi::StringMap> m_properties; + wpi::StringMap m_properties; bool m_metadataDirty = true; int m_column = -1; int m_row = -1; @@ -49,7 +49,7 @@ class ShuffleboardComponentBase : public virtual ShuffleboardValue { /** * Gets the custom properties for this component. May be null. */ - const wpi::StringMap>& GetProperties() const; + const wpi::StringMap& GetProperties() const; }; } // namespace frc diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardContainer.h b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardContainer.h index 13a5cee21e..719bf9ed5e 100644 --- a/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardContainer.h +++ b/wpilibc/src/main/native/include/frc/shuffleboard/ShuffleboardContainer.h @@ -171,8 +171,7 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { * @see AddPersistent(std::string_view, std::shared_ptr) * Add(std::string_view title, std::shared_ptr defaultValue) */ - SimpleWidget& Add(std::string_view title, - std::shared_ptr defaultValue); + SimpleWidget& Add(std::string_view title, const nt::Value& defaultValue); /** * Adds a widget to this container to display the given data. @@ -200,6 +199,19 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { */ SimpleWidget& Add(std::string_view title, double defaultValue); + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see AddPersistent(std::string_view, double) + * Add(std::string_view title, double defaultValue) + */ + SimpleWidget& Add(std::string_view title, float defaultValue); + /** * Adds a widget to this container to display the given data. * @@ -266,6 +278,34 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { SimpleWidget& Add(std::string_view title, wpi::span defaultValue); + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see AddPersistent(std::string_view, wpi::span) + * Add(std::string_view title, wpi::span defaultValue) + */ + SimpleWidget& Add(std::string_view title, + wpi::span defaultValue); + + /** + * Adds a widget to this container to display the given data. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @throws IllegalArgumentException if a widget already exists in this + * container with the given title + * @see AddPersistent(std::string_view, wpi::span) + * Add(std::string_view title, wpi::span defaultValue) + */ + SimpleWidget& Add(std::string_view title, + wpi::span defaultValue); + /** * Adds a widget to this container to display the given data. * @@ -306,6 +346,45 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { SuppliedValueWidget& AddNumber(std::string_view title, std::function supplier); + /** + * Adds a widget to this container. The widget will display the data provided + * by the value supplier. Changes made on the dashboard will not propagate to + * the widget object, and will be overridden by values from the value + * supplier. + * + * @param title the title of the widget + * @param supplier the supplier for values + * @return a widget to display data + */ + SuppliedValueWidget& AddDouble(std::string_view title, + std::function supplier); + + /** + * Adds a widget to this container. The widget will display the data provided + * by the value supplier. Changes made on the dashboard will not propagate to + * the widget object, and will be overridden by values from the value + * supplier. + * + * @param title the title of the widget + * @param supplier the supplier for values + * @return a widget to display data + */ + SuppliedValueWidget& AddFloat(std::string_view title, + std::function supplier); + + /** + * Adds a widget to this container. The widget will display the data provided + * by the value supplier. Changes made on the dashboard will not propagate to + * the widget object, and will be overridden by values from the value + * supplier. + * + * @param title the title of the widget + * @param supplier the supplier for values + * @return a widget to display data + */ + SuppliedValueWidget& AddInteger(std::string_view title, + std::function supplier); + /** * Adds a widget to this container. The widget will display the data provided * by the value supplier. Changes made on the dashboard will not propagate to @@ -346,6 +425,45 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { SuppliedValueWidget>& AddNumberArray( std::string_view title, std::function()> supplier); + /** + * Adds a widget to this container. The widget will display the data provided + * by the value supplier. Changes made on the dashboard will not propagate to + * the widget object, and will be overridden by values from the value + * supplier. + * + * @param title the title of the widget + * @param supplier the supplier for values + * @return a widget to display data + */ + SuppliedValueWidget>& AddDoubleArray( + std::string_view title, std::function()> supplier); + + /** + * Adds a widget to this container. The widget will display the data provided + * by the value supplier. Changes made on the dashboard will not propagate to + * the widget object, and will be overridden by values from the value + * supplier. + * + * @param title the title of the widget + * @param supplier the supplier for values + * @return a widget to display data + */ + SuppliedValueWidget>& AddFloatArray( + std::string_view title, std::function()> supplier); + + /** + * Adds a widget to this container. The widget will display the data provided + * by the value supplier. Changes made on the dashboard will not propagate to + * the widget object, and will be overridden by values from the value + * supplier. + * + * @param title the title of the widget + * @param supplier the supplier for values + * @return a widget to display data + */ + SuppliedValueWidget>& AddIntegerArray( + std::string_view title, std::function()> supplier); + /** * Adds a widget to this container. The widget will display the data provided * by the value supplier. Changes made on the dashboard will not propagate to @@ -369,8 +487,23 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { * @param supplier the supplier for values * @return a widget to display data */ - SuppliedValueWidget& AddRaw( - std::string_view title, std::function supplier); + SuppliedValueWidget>& AddRaw( + std::string_view title, std::function()> supplier); + + /** + * Adds a widget to this container. The widget will display the data provided + * by the value supplier. Changes made on the dashboard will not propagate to + * the widget object, and will be overridden by values from the value + * supplier. + * + * @param title the title of the widget + * @param typeString the NT type string + * @param supplier the supplier for values + * @return a widget to display data + */ + SuppliedValueWidget>& AddRaw( + std::string_view title, std::string_view typeString, + std::function()> supplier); /** * Adds a widget to this container to display a simple piece of data. @@ -386,7 +519,7 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { * Add(std::string_view title, std::shared_ptr defaultValue) */ SimpleWidget& AddPersistent(std::string_view title, - std::shared_ptr defaultValue); + const nt::Value& defaultValue); /** * Adds a widget to this container to display a simple piece of data. @@ -421,15 +554,30 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { /** * Adds a widget to this container to display a simple piece of data. * - * Unlike Add(std::string_view, int), the value in the widget will be saved on - * the robot and will be used when the robot program next starts rather than - * {@code defaultValue}. + * Unlike Add(std::string_view, float), the value in the widget will be saved + * on the robot and will be used when the robot program next starts rather + * than {@code defaultValue}. * * @param title the title of the widget * @param defaultValue the default value of the widget * @return a widget to display the sendable data - * @see Add(std:string_view, int) - * Add(std::string_view title, int defaultValue) + * @see Add(std::string_view, float) + * Add(std::string_view title, float defaultValue) + */ + SimpleWidget& AddPersistent(std::string_view title, float defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike Add(std::string_view, int64_t), the value in the widget will be + * saved on the robot and will be used when the robot program next starts + * rather than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see Add(std:string_view, int64_t) + * Add(std::string_view title, int64_t defaultValue) */ SimpleWidget& AddPersistent(std::string_view title, int defaultValue); @@ -481,6 +629,38 @@ class ShuffleboardContainer : public virtual ShuffleboardValue { SimpleWidget& AddPersistent(std::string_view title, wpi::span defaultValue); + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike Add(std::string_view, wpi::span), the value in the + * widget will be saved on the robot and will be used when the robot program + * next starts rather than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see Add(std::string_view, wpi::span) + * Add(std::string_view title, wpi::span defaultValue) + */ + SimpleWidget& AddPersistent(std::string_view title, + wpi::span defaultValue); + + /** + * Adds a widget to this container to display a simple piece of data. + * + * Unlike Add(std::string_view, wpi::span), the value in the + * widget will be saved on the robot and will be used when the robot program + * next starts rather than {@code defaultValue}. + * + * @param title the title of the widget + * @param defaultValue the default value of the widget + * @return a widget to display the sendable data + * @see Add(std::string_view, wpi::span) + * Add(std::string_view title, wpi::span defaultValue) + */ + SimpleWidget& AddPersistent(std::string_view title, + wpi::span defaultValue); + /** * Adds a widget to this container to display a simple piece of data. * diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/SimpleWidget.h b/wpilibc/src/main/native/include/frc/shuffleboard/SimpleWidget.h index 746b7d6b3d..8653549ff2 100644 --- a/wpilibc/src/main/native/include/frc/shuffleboard/SimpleWidget.h +++ b/wpilibc/src/main/native/include/frc/shuffleboard/SimpleWidget.h @@ -5,10 +5,11 @@ #pragma once #include +#include #include +#include #include -#include #include "frc/shuffleboard/ShuffleboardWidget.h" @@ -26,14 +27,24 @@ class SimpleWidget final : public ShuffleboardWidget { /** * Gets the NetworkTable entry that contains the data for this widget. + * The widget owns the entry. */ - nt::NetworkTableEntry GetEntry(); + nt::GenericEntry& GetEntry(); + + /** + * Gets the NetworkTable entry that contains the data for this widget. + * The widget owns the entry. + * + * @param typeString NT type string + */ + nt::GenericEntry& GetEntry(std::string_view typeString); void BuildInto(std::shared_ptr parentTable, std::shared_ptr metaTable) override; private: - nt::NetworkTableEntry m_entry; + nt::GenericEntry m_entry; + std::string m_typeString; void ForceGenerate(); }; diff --git a/wpilibc/src/main/native/include/frc/shuffleboard/SuppliedValueWidget.h b/wpilibc/src/main/native/include/frc/shuffleboard/SuppliedValueWidget.h index a2b042ef5a..ae69ab434f 100644 --- a/wpilibc/src/main/native/include/frc/shuffleboard/SuppliedValueWidget.h +++ b/wpilibc/src/main/native/include/frc/shuffleboard/SuppliedValueWidget.h @@ -6,10 +6,12 @@ #include #include +#include #include +#include +#include #include -#include #include "frc/shuffleboard/ShuffleboardComponent.h" #include "frc/shuffleboard/ShuffleboardComponent.inc" @@ -23,24 +25,31 @@ template class SuppliedValueWidget : public ShuffleboardWidget> { public: SuppliedValueWidget(ShuffleboardContainer& parent, std::string_view title, - std::function supplier, - std::function setter) + std::string_view typeString, std::function supplier, + std::function setter) : ShuffleboardValue(title), ShuffleboardWidget>(parent, title), + m_typeString(typeString), m_supplier(supplier), m_setter(setter) {} void BuildInto(std::shared_ptr parentTable, std::shared_ptr metaTable) override { this->BuildMetadata(metaTable); - metaTable->GetEntry("Controllable").SetBoolean(false); + m_controllablePub = + nt::BooleanTopic{metaTable->GetTopic("Controllable")}.Publish(); + m_controllablePub.Set(false); - auto entry = parentTable->GetEntry(this->GetTitle()); - m_setter(entry, m_supplier()); + m_entry = + parentTable->GetTopic(this->GetTitle()).GenericPublish(m_typeString); + m_setter(m_entry, m_supplier()); } private: + std::string m_typeString; std::function m_supplier; - std::function m_setter; + std::function m_setter; + nt::BooleanPublisher m_controllablePub; + nt::GenericPublisher m_entry; }; } // namespace frc diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/FieldObject2d.h b/wpilibc/src/main/native/include/frc/smartdashboard/FieldObject2d.h index 2e41d843a0..83bdc9d392 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/FieldObject2d.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/FieldObject2d.h @@ -10,7 +10,7 @@ #include #include -#include +#include #include #include #include @@ -105,7 +105,7 @@ class FieldObject2d { mutable wpi::mutex m_mutex; std::string m_name; - nt::NetworkTableEntry m_entry; + nt::DoubleArrayEntry m_entry; mutable wpi::SmallVector m_poses; }; diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/Mechanism2d.h b/wpilibc/src/main/native/include/frc/smartdashboard/Mechanism2d.h index eef7a3076b..c71bcaf75c 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/Mechanism2d.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/Mechanism2d.h @@ -7,8 +7,10 @@ #include #include +#include #include -#include +#include +#include #include #include #include @@ -83,5 +85,7 @@ class Mechanism2d : public nt::NTSendable, mutable wpi::mutex m_mutex; std::shared_ptr m_table; wpi::StringMap> m_roots; + nt::DoubleArrayPublisher m_dimsPub; + nt::StringPublisher m_colorPub; }; } // namespace frc diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/MechanismLigament2d.h b/wpilibc/src/main/native/include/frc/smartdashboard/MechanismLigament2d.h index f1bebb9a67..f9c86fc23e 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/MechanismLigament2d.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/MechanismLigament2d.h @@ -7,7 +7,8 @@ #include #include -#include +#include +#include #include #include "frc/smartdashboard/MechanismObject2d.h" @@ -89,14 +90,14 @@ class MechanismLigament2d : public MechanismObject2d { void UpdateEntries(std::shared_ptr table) override; private: - void Flush(); + nt::StringPublisher m_typePub; double m_length; - nt::NetworkTableEntry m_lengthEntry; + nt::DoubleEntry m_lengthEntry; double m_angle; - nt::NetworkTableEntry m_angleEntry; + nt::DoubleEntry m_angleEntry; double m_weight; - nt::NetworkTableEntry m_weightEntry; + nt::DoubleEntry m_weightEntry; char m_color[10]; - nt::NetworkTableEntry m_colorEntry; + nt::StringEntry m_colorEntry; }; } // namespace frc diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/MechanismRoot2d.h b/wpilibc/src/main/native/include/frc/smartdashboard/MechanismRoot2d.h index 5072547fd6..37e0f7bd8e 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/MechanismRoot2d.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/MechanismRoot2d.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include "MechanismObject2d.h" @@ -48,7 +48,7 @@ class MechanismRoot2d : private MechanismObject2d { inline void Flush(); double m_x; double m_y; - nt::NetworkTableEntry m_xEntry; - nt::NetworkTableEntry m_yEntry; + nt::DoublePublisher m_xPub; + nt::DoublePublisher m_yPub; }; } // namespace frc diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/SendableBuilderImpl.h b/wpilibc/src/main/native/include/frc/smartdashboard/SendableBuilderImpl.h index 36830f6dc1..d156ca1b01 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/SendableBuilderImpl.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/SendableBuilderImpl.h @@ -11,10 +11,11 @@ #include #include +#include #include #include -#include -#include +#include +#include #include #include @@ -54,7 +55,8 @@ class SendableBuilderImpl : public nt::NTSendableBuilder { bool IsActuator() const; /** - * Update the network table values by calling the getters for all properties. + * Synchronize with network table values by calling the getters for all + * properties and setters when the network table value has changed. */ void Update() override; @@ -88,12 +90,18 @@ class SendableBuilderImpl : public nt::NTSendableBuilder { void SetSmartDashboardType(std::string_view type) override; void SetActuator(bool value) override; void SetSafeState(std::function func) override; - void SetUpdateTable(std::function func) override; - nt::NetworkTableEntry GetEntry(std::string_view key) override; + void SetUpdateTable(wpi::unique_function func) override; + nt::Topic GetTopic(std::string_view key) override; void AddBooleanProperty(std::string_view key, std::function getter, std::function setter) override; + void AddIntegerProperty(std::string_view key, std::function getter, + std::function setter) override; + + void AddFloatProperty(std::string_view key, std::function getter, + std::function setter) override; + void AddDoubleProperty(std::string_view key, std::function getter, std::function setter) override; @@ -105,6 +113,14 @@ class SendableBuilderImpl : public nt::NTSendableBuilder { std::string_view key, std::function()> getter, std::function)> setter) override; + void AddIntegerArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) override; + + void AddFloatArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) override; + void AddDoubleArrayProperty( std::string_view key, std::function()> getter, std::function)> setter) override; @@ -113,12 +129,10 @@ class SendableBuilderImpl : public nt::NTSendableBuilder { std::string_view key, std::function()> getter, std::function)> setter) override; - void AddRawProperty(std::string_view key, std::function getter, - std::function setter) override; - - void AddValueProperty( - std::string_view key, std::function()> getter, - std::function)> setter) override; + void AddRawProperty( + std::string_view key, std::string_view typeString, + std::function()> getter, + std::function)> setter) override; void AddSmallStringProperty( std::string_view key, @@ -131,6 +145,19 @@ class SendableBuilderImpl : public nt::NTSendableBuilder { getter, std::function)> setter) override; + void AddSmallIntegerArrayProperty( + std::string_view key, + std::function< + wpi::span(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) override; + + void AddSmallFloatArrayProperty( + std::string_view key, + std::function(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) override; + void AddSmallDoubleArrayProperty( std::string_view key, std::function(wpi::SmallVectorImpl& buf)> @@ -145,64 +172,46 @@ class SendableBuilderImpl : public nt::NTSendableBuilder { std::function)> setter) override; void AddSmallRawProperty( - std::string_view key, - std::function& buf)> getter, - std::function setter) override; + std::string_view key, std::string_view typeString, + std::function(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) override; private: struct Property { - Property(nt::NetworkTable& table, std::string_view key) - : entry(table.GetEntry(key)) {} - - Property(const Property&) = delete; - Property& operator=(const Property&) = delete; - - Property(Property&& other) noexcept - : entry(other.entry), - listener(other.listener), - update(std::move(other.update)), - createListener(std::move(other.createListener)) { - other.entry = nt::NetworkTableEntry(); - other.listener = 0; - } - - Property& operator=(Property&& other) noexcept { - entry = other.entry; - listener = other.listener; - other.entry = nt::NetworkTableEntry(); - other.listener = 0; - update = std::move(other.update); - createListener = std::move(other.createListener); - return *this; - } - - ~Property() { StopListener(); } - - void StartListener() { - if (entry && listener == 0 && createListener) { - listener = createListener(entry); - } - } - - void StopListener() { - if (entry && listener != 0) { - entry.RemoveListener(listener); - listener = 0; - } - } - - nt::NetworkTableEntry entry; - NT_EntryListener listener = 0; - std::function update; - std::function createListener; + virtual ~Property() = default; + virtual void Update(bool controllable, int64_t time) = 0; }; - std::vector m_properties; + template + struct PropertyImpl : public Property { + void Update(bool controllable, int64_t time) override; + + using Publisher = typename Topic::PublisherType; + using Subscriber = typename Topic::SubscriberType; + Publisher pub; + Subscriber sub; + std::function updateNetwork; + std::function updateLocal; + }; + + template + void AddPropertyImpl(Topic topic, Getter getter, Setter setter); + + template + void AddSmallPropertyImpl(Topic topic, Getter getter, Setter setter); + + std::vector> m_properties; std::function m_safeState; - std::vector> m_updateTables; + std::vector> m_updateTables; std::shared_ptr m_table; - nt::NetworkTableEntry m_controllableEntry; + bool m_controllable = false; bool m_actuator = false; + + nt::BooleanPublisher m_controllablePublisher; + nt::StringPublisher m_typePublisher; + nt::BooleanPublisher m_actuatorPublisher; }; } // namespace frc diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc index 25e25511c1..7bc30cbfb8 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc +++ b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooser.inc @@ -48,7 +48,14 @@ auto SendableChooser::GetSelected() template void SendableChooser::InitSendable(nt::NTSendableBuilder& builder) { builder.SetSmartDashboardType("String Chooser"); - builder.GetEntry(kInstance).SetDouble(m_instance); + { + std::scoped_lock lock(m_mutex); + m_instancePubs.emplace_back( + nt::IntegerTopic{builder.GetTopic(kInstance)}.Publish()); + m_instancePubs.back().Set(m_instance); + m_activePubs.emplace_back( + nt::StringTopic{builder.GetTopic(kActive)}.Publish()); + } builder.AddStringArrayProperty( kOptions, [=] { @@ -82,16 +89,12 @@ void SendableChooser::InitSendable(nt::NTSendableBuilder& builder) { } }, nullptr); - { - std::scoped_lock lock(m_mutex); - m_activeEntries.emplace_back(builder.GetEntry(kActive)); - } builder.AddStringProperty(kSelected, nullptr, [=](std::string_view val) { std::scoped_lock lock(m_mutex); m_haveSelected = true; m_selected = val; - for (auto& entry : m_activeEntries) { - entry.SetString(val); + for (auto& pub : m_activePubs) { + pub.Set(val); } }); } diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h index 78f891a461..b5df73cc1a 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/SendableChooserBase.h @@ -7,8 +7,9 @@ #include #include +#include #include -#include +#include #include #include #include @@ -40,7 +41,8 @@ class SendableChooserBase : public nt::NTSendable, std::string m_defaultChoice; std::string m_selected; bool m_haveSelected = false; - wpi::SmallVector m_activeEntries; + wpi::SmallVector m_instancePubs; + wpi::SmallVector m_activePubs; wpi::mutex m_mutex; int m_instance; static std::atomic_int s_instances; diff --git a/wpilibc/src/main/native/include/frc/smartdashboard/SmartDashboard.h b/wpilibc/src/main/native/include/frc/smartdashboard/SmartDashboard.h index 47c4a28e76..2e352156f1 100644 --- a/wpilibc/src/main/native/include/frc/smartdashboard/SmartDashboard.h +++ b/wpilibc/src/main/native/include/frc/smartdashboard/SmartDashboard.h @@ -62,39 +62,6 @@ class SmartDashboard { */ static bool IsPersistent(std::string_view key); - /** - * 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) - */ - static 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) - */ - static 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 - */ - static unsigned int GetFlags(std::string_view key); - - /** - * Deletes the specified key in this table. - * - * @param key the key name - */ - static void Delete(std::string_view key); - /** * Returns an NT Entry mapping to the specified key * @@ -363,7 +330,7 @@ class SmartDashboard { * @param value The value that will be assigned. * @return False if the table key already exists with a different type */ - static bool PutRaw(std::string_view key, std::string_view value); + static bool PutRaw(std::string_view key, wpi::span value); /** * Gets the current value in the table, setting it if it does not exist. @@ -373,7 +340,7 @@ class SmartDashboard { * @returns False if the table key exists with a different type */ static bool SetDefaultRaw(std::string_view key, - std::string_view defaultValue); + wpi::span defaultValue); /** * Returns the raw value (byte array) the key maps to. @@ -389,8 +356,8 @@ class SmartDashboard { * @note This makes a copy of the raw contents. If the overhead of this is a * concern, use GetValue() instead. */ - static std::string GetRaw(std::string_view key, - std::string_view defaultValue); + static std::vector GetRaw(std::string_view key, + wpi::span defaultValue); /** * Maps the specified key to the specified complex value (such as an array) in @@ -403,8 +370,7 @@ class SmartDashboard { * @param value the value * @return False if the table key already exists with a different type */ - static bool PutValue(std::string_view keyName, - std::shared_ptr value); + static bool PutValue(std::string_view keyName, const nt::Value& value); /** * Gets the current value in the table, setting it if it does not exist. @@ -414,7 +380,7 @@ class SmartDashboard { * @returns False if the table key exists with a different type */ static bool SetDefaultValue(std::string_view key, - std::shared_ptr defaultValue); + const nt::Value& defaultValue); /** * Retrieves the complex value (such as an array) in this table into the @@ -422,7 +388,7 @@ class SmartDashboard { * * @param keyName the key */ - static std::shared_ptr GetValue(std::string_view keyName); + static nt::Value GetValue(std::string_view keyName); /** * Posts a task from a listener to the ListenerExecutor, so that it can be run diff --git a/wpilibc/src/test/native/cpp/event/NetworkBooleanEventTest.cpp b/wpilibc/src/test/native/cpp/event/NetworkBooleanEventTest.cpp new file mode 100644 index 0000000000..d2ac7c43b2 --- /dev/null +++ b/wpilibc/src/test/native/cpp/event/NetworkBooleanEventTest.cpp @@ -0,0 +1,46 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include +#include + +#include "frc/event/EventLoop.h" +#include "frc/event/NetworkBooleanEvent.h" +#include "gtest/gtest.h" + +using namespace frc; + +class NetworkBooleanEventTest : public ::testing::Test { + public: + NetworkBooleanEventTest() { + m_inst = nt::NetworkTableInstance::Create(); + m_inst.StartLocal(); + } + + ~NetworkBooleanEventTest() override { + nt::NetworkTableInstance::Destroy(m_inst); + } + + nt::NetworkTableInstance m_inst; +}; + +TEST_F(NetworkBooleanEventTest, Set) { + EventLoop loop; + int counter = 0; + + auto pub = m_inst.GetTable("TestTable")->GetBooleanTopic("Test").Publish(); + + NetworkBooleanEvent(&loop, m_inst, "TestTable", "Test").IfHigh([&] { + ++counter; + }); + pub.Set(false); + loop.Poll(); + EXPECT_EQ(0, counter); + pub.Set(true); + loop.Poll(); + EXPECT_EQ(1, counter); + pub.Set(false); + loop.Poll(); + EXPECT_EQ(1, counter); +} diff --git a/wpilibc/src/test/native/cpp/shuffleboard/ShuffleboardInstanceTest.cpp b/wpilibc/src/test/native/cpp/shuffleboard/ShuffleboardInstanceTest.cpp index d370a2d3b4..606b8f06eb 100644 --- a/wpilibc/src/test/native/cpp/shuffleboard/ShuffleboardInstanceTest.cpp +++ b/wpilibc/src/test/native/cpp/shuffleboard/ShuffleboardInstanceTest.cpp @@ -25,14 +25,14 @@ TEST(ShuffleboardInstanceTest, PathFluent) { NTWrapper ntInst; frc::detail::ShuffleboardInstance shuffleboardInst{ntInst.inst}; - auto entry = shuffleboardInst.GetTab("Tab Title") - .GetLayout("List", "List Layout") - .Add("Data", "string") - .WithWidget("Text View") - .GetEntry(); + auto& entry = shuffleboardInst.GetTab("Tab Title") + .GetLayout("List", "List Layout") + .Add("Data", "string") + .WithWidget("Text View") + .GetEntry(); EXPECT_EQ("string", entry.GetString("")) << "Wrong entry value"; - EXPECT_EQ("/Shuffleboard/Tab Title/List/Data", entry.GetName()) + EXPECT_EQ("/Shuffleboard/Tab Title/List/Data", entry.GetTopic().GetName()) << "Entry path generated incorrectly"; } @@ -40,17 +40,17 @@ TEST(ShuffleboardInstanceTest, NestedLayoutsFluent) { NTWrapper ntInst; frc::detail::ShuffleboardInstance shuffleboardInst{ntInst.inst}; - auto entry = shuffleboardInst.GetTab("Tab") - .GetLayout("First", "List") - .GetLayout("Second", "List") - .GetLayout("Third", "List") - .GetLayout("Fourth", "List") - .Add("Value", "string") - .GetEntry(); + auto& entry = shuffleboardInst.GetTab("Tab") + .GetLayout("First", "List") + .GetLayout("Second", "List") + .GetLayout("Third", "List") + .GetLayout("Fourth", "List") + .Add("Value", "string") + .GetEntry(); EXPECT_EQ("string", entry.GetString("")) << "Wrong entry value"; EXPECT_EQ("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", - entry.GetName()) + entry.GetTopic().GetName()) << "Entry path generated incorrectly"; } @@ -64,11 +64,11 @@ TEST(ShuffleboardInstanceTest, NestedLayoutsOop) { frc::ShuffleboardLayout& third = second.GetLayout("Third", "List"); frc::ShuffleboardLayout& fourth = third.GetLayout("Fourth", "List"); frc::SimpleWidget& widget = fourth.Add("Value", "string"); - auto entry = widget.GetEntry(); + auto& entry = widget.GetEntry(); EXPECT_EQ("string", entry.GetString("")) << "Wrong entry value"; EXPECT_EQ("/Shuffleboard/Tab/First/Second/Third/Fourth/Value", - entry.GetName()) + entry.GetTopic().GetName()) << "Entry path generated incorrectly"; } @@ -97,12 +97,12 @@ TEST(ShuffleboardInstanceTest, NestedActuatorWidgetsAreDisabled) { // Note: we use the unsafe `GetBoolean()` method because if the value is NOT // a boolean, or if it is not present, then something has clearly gone very, // very wrong - bool controllable = controllableEntry.GetValue()->GetBoolean(); + bool controllable = controllableEntry.GetValue().GetBoolean(); // Sanity check EXPECT_TRUE(controllable) << "The nested actuator widget should be enabled by default"; shuffleboardInst.DisableActuatorWidgets(); - controllable = controllableEntry.GetValue()->GetBoolean(); + controllable = controllableEntry.GetValue().GetBoolean(); EXPECT_FALSE(controllable) << "The nested actuator widget should have been disabled"; } diff --git a/wpilibc/src/test/native/cpp/shuffleboard/SuppliedValueWidgetTest.cpp b/wpilibc/src/test/native/cpp/shuffleboard/SuppliedValueWidgetTest.cpp index c8449e5be7..d964639b6e 100644 --- a/wpilibc/src/test/native/cpp/shuffleboard/SuppliedValueWidgetTest.cpp +++ b/wpilibc/src/test/native/cpp/shuffleboard/SuppliedValueWidgetTest.cpp @@ -33,7 +33,7 @@ TEST_F(SuppliedValueWidgetTest, AddString) { auto entry = m_ntInst.inst.GetEntry("/Shuffleboard/Tab/String"); m_shuffleboardInst.Update(); - EXPECT_EQ("foo", entry.GetValue()->GetString()); + EXPECT_EQ("foo", entry.GetValue().GetString()); } TEST_F(SuppliedValueWidgetTest, AddNumber) { @@ -42,7 +42,7 @@ TEST_F(SuppliedValueWidgetTest, AddNumber) { auto entry = m_ntInst.inst.GetEntry("/Shuffleboard/Tab/Num"); m_shuffleboardInst.Update(); - EXPECT_FLOAT_EQ(1.0, entry.GetValue()->GetDouble()); + EXPECT_FLOAT_EQ(1.0, entry.GetValue().GetDouble()); } TEST_F(SuppliedValueWidgetTest, AddBoolean) { @@ -51,7 +51,7 @@ TEST_F(SuppliedValueWidgetTest, AddBoolean) { auto entry = m_ntInst.inst.GetEntry("/Shuffleboard/Tab/Bool"); m_shuffleboardInst.Update(); - EXPECT_EQ(true, entry.GetValue()->GetBoolean()); + EXPECT_EQ(true, entry.GetValue().GetBoolean()); } TEST_F(SuppliedValueWidgetTest, AddStringArray) { @@ -60,7 +60,7 @@ TEST_F(SuppliedValueWidgetTest, AddStringArray) { auto entry = m_ntInst.inst.GetEntry("/Shuffleboard/Tab/Strings"); m_shuffleboardInst.Update(); - auto actual = entry.GetValue()->GetStringArray(); + auto actual = entry.GetValue().GetStringArray(); EXPECT_EQ(strings.size(), actual.size()); for (size_t i = 0; i < strings.size(); i++) { @@ -74,7 +74,7 @@ TEST_F(SuppliedValueWidgetTest, AddNumberArray) { auto entry = m_ntInst.inst.GetEntry("/Shuffleboard/Tab/Numbers"); m_shuffleboardInst.Update(); - auto actual = entry.GetValue()->GetDoubleArray(); + auto actual = entry.GetValue().GetDoubleArray(); EXPECT_EQ(nums.size(), actual.size()); for (size_t i = 0; i < nums.size(); i++) { @@ -88,7 +88,7 @@ TEST_F(SuppliedValueWidgetTest, AddBooleanArray) { auto entry = m_ntInst.inst.GetEntry("/Shuffleboard/Tab/Booleans"); m_shuffleboardInst.Update(); - auto actual = entry.GetValue()->GetBooleanArray(); + auto actual = entry.GetValue().GetBooleanArray(); EXPECT_EQ(bools.size(), actual.size()); for (size_t i = 0; i < bools.size(); i++) { @@ -97,11 +97,11 @@ TEST_F(SuppliedValueWidgetTest, AddBooleanArray) { } TEST_F(SuppliedValueWidgetTest, AddRaw) { - std::string_view bytes = "\1\2\3"; + std::vector bytes = {1, 2, 3}; m_tab->AddRaw("Raw", [&bytes]() { return bytes; }); auto entry = m_ntInst.inst.GetEntry("/Shuffleboard/Tab/Raw"); m_shuffleboardInst.Update(); - auto actual = entry.GetValue()->GetRaw(); - EXPECT_EQ(bytes, actual); + auto actual = entry.GetValue().GetRaw(); + EXPECT_EQ(bytes, std::vector(actual.begin(), actual.end())); } diff --git a/wpilibcExamples/build.gradle b/wpilibcExamples/build.gradle index b373657819..4c6f92ee9a 100644 --- a/wpilibcExamples/build.gradle +++ b/wpilibcExamples/build.gradle @@ -62,7 +62,7 @@ model { lib project: ':wpilibNewCommands', library: 'wpilibNewCommands', linkage: 'shared' lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(binary, 'shared') lib project: ':cscore', library: 'cscore', linkage: 'shared' project(':hal').addHalDependency(binary, 'shared') lib project: ':cameraserver', library: 'cameraserver', linkage: 'shared' @@ -90,7 +90,7 @@ model { lib project: ':wpilibNewCommands', library: 'wpilibNewCommands', linkage: 'shared' lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(binary, 'shared') lib project: ':cscore', library: 'cscore', linkage: 'shared' project(':hal').addHalDependency(binary, 'shared') lib project: ':cameraserver', library: 'cameraserver', linkage: 'shared' @@ -138,7 +138,7 @@ model { lib project: ':wpilibNewCommands', library: 'wpilibNewCommands', linkage: 'shared' lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(binary, 'shared') lib project: ':cscore', library: 'cscore', linkage: 'shared' project(':hal').addHalDependency(binary, 'shared') lib project: ':cameraserver', library: 'cameraserver', linkage: 'shared' @@ -203,7 +203,7 @@ model { lib project: ':wpilibNewCommands', library: 'wpilibNewCommands', linkage: 'shared' lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') lib project: ':cscore', library: 'cscore', linkage: 'shared' project(':hal').addHalDependency(it, 'shared') lib project: ':cameraserver', library: 'cameraserver', linkage: 'shared' diff --git a/wpilibcExamples/src/main/cpp/examples/ShuffleBoard/cpp/Robot.cpp b/wpilibcExamples/src/main/cpp/examples/ShuffleBoard/cpp/Robot.cpp index a114b927ae..6cb97938f6 100644 --- a/wpilibcExamples/src/main/cpp/examples/ShuffleBoard/cpp/Robot.cpp +++ b/wpilibcExamples/src/main/cpp/examples/ShuffleBoard/cpp/Robot.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include /** @@ -31,10 +31,10 @@ class Robot : public frc::TimedRobot { public: void RobotInit() override { // Add a widget titled 'Max Speed' with a number slider. - m_maxSpeed = frc::Shuffleboard::GetTab("Configuration") - .Add("Max Speed", 1) - .WithWidget("Number Slider") - .GetEntry(); + m_maxSpeed = &frc::Shuffleboard::GetTab("Configuration") + .Add("Max Speed", 1) + .WithWidget("Number Slider") + .GetEntry(); // Create a 'DriveBase' tab and add the drivetrain object to it. frc::ShuffleboardTab& driveBaseTab = frc::Shuffleboard::GetTab("DriveBase"); @@ -57,7 +57,7 @@ class Robot : public frc::TimedRobot { void AutonomousInit() override { // Update the Max Output for the drivetrain. - m_robotDrive.SetMaxOutput(m_maxSpeed.GetDouble(1.0)); + m_robotDrive.SetMaxOutput(m_maxSpeed->GetDouble(1.0)); } private: @@ -73,7 +73,7 @@ class Robot : public frc::TimedRobot { frc::Encoder m_rightEncoder{2, 3}; frc::AnalogPotentiometer m_ElevatorPot{0}; - nt::NetworkTableEntry m_maxSpeed; + nt::GenericEntry* m_maxSpeed; }; #ifndef RUNNING_FRC_TESTS diff --git a/wpilibcExamples/src/main/cpp/examples/SimpleDifferentialDriveSimulation/cpp/Robot.cpp b/wpilibcExamples/src/main/cpp/examples/SimpleDifferentialDriveSimulation/cpp/Robot.cpp index afad9992b9..7ea7b091bb 100644 --- a/wpilibcExamples/src/main/cpp/examples/SimpleDifferentialDriveSimulation/cpp/Robot.cpp +++ b/wpilibcExamples/src/main/cpp/examples/SimpleDifferentialDriveSimulation/cpp/Robot.cpp @@ -14,10 +14,6 @@ class Robot : public frc::TimedRobot { public: void RobotInit() override { - // Flush NetworkTables every loop. This ensures that robot pose and other - // values are sent during every iteration. - SetNetworkTablesFlushEnabled(true); - m_trajectory = frc::TrajectoryGenerator::GenerateTrajectory( frc::Pose2d{2_m, 2_m, 0_rad}, {}, frc::Pose2d{6_m, 4_m, 0_rad}, frc::TrajectoryConfig(2_mps, 2_mps_sq)); diff --git a/wpilibcExamples/src/main/cpp/examples/StateSpaceDifferentialDriveSimulation/cpp/Robot.cpp b/wpilibcExamples/src/main/cpp/examples/StateSpaceDifferentialDriveSimulation/cpp/Robot.cpp index 318dc6155b..fce5aececc 100644 --- a/wpilibcExamples/src/main/cpp/examples/StateSpaceDifferentialDriveSimulation/cpp/Robot.cpp +++ b/wpilibcExamples/src/main/cpp/examples/StateSpaceDifferentialDriveSimulation/cpp/Robot.cpp @@ -9,11 +9,7 @@ #include #include -void Robot::RobotInit() { - // Flush NetworkTables every loop. This ensures that robot pose and other - // values are sent during every iteration. - SetNetworkTablesFlushEnabled(true); -} +void Robot::RobotInit() {} /** * This function is called every 20 ms, no matter the mode. Use diff --git a/wpilibcIntegrationTests/build.gradle b/wpilibcIntegrationTests/build.gradle index 4ae871d67a..9cd75f849c 100644 --- a/wpilibcIntegrationTests/build.gradle +++ b/wpilibcIntegrationTests/build.gradle @@ -41,9 +41,9 @@ model { } lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(binary, 'shared') + project(':ntcore').addNtcoreJniDependency(binary) lib project: ':cscore', library: 'cscore', linkage: 'shared' - lib project: ':ntcore', library: 'ntcoreJNIShared', linkage: 'shared' lib project: ':cscore', library: 'cscoreJNIShared', linkage: 'shared' project(':hal').addHalDependency(binary, 'shared') project(':hal').addHalJniDependency(binary) diff --git a/wpilibcIntegrationTests/src/main/native/cpp/PreferencesTest.cpp b/wpilibcIntegrationTests/src/main/native/cpp/PreferencesTest.cpp index 5e79f27460..78d9c43837 100644 --- a/wpilibcIntegrationTests/src/main/native/cpp/PreferencesTest.cpp +++ b/wpilibcIntegrationTests/src/main/native/cpp/PreferencesTest.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -14,7 +15,7 @@ #include "frc/Timer.h" #include "gtest/gtest.h" -static const char* kFileName = "networktables.ini"; +static const char* kFileName = "networktables.json"; static constexpr auto kSaveTime = 1.2_s; /** @@ -27,47 +28,71 @@ TEST(PreferencesTest, ReadPreferencesFromFile) { std::remove(kFileName); std::ofstream preferencesFile(kFileName); - preferencesFile << "[NetworkTables Storage 3.0]" << std::endl; - preferencesFile - << "string \"/Preferences/testFileGetString\"=\"Hello, preferences file\"" - << std::endl; - preferencesFile << "double \"/Preferences/testFileGetInt\"=1" << std::endl; - preferencesFile << "double \"/Preferences/testFileGetDouble\"=0.5" - << std::endl; - preferencesFile << "double \"/Preferences/testFileGetFloat\"=0.25" - << std::endl; - preferencesFile << "boolean \"/Preferences/testFileGetBoolean\"=true" - << std::endl; - preferencesFile - << "double \"/Preferences/testFileGetLong\"=1000000000000000000" - << std::endl; + preferencesFile << "[" << std::endl; + preferencesFile << "{\"type\":\"string\"," + << "\"name\":\"/Preferences/testFileGetString\"," + << "\"value\":\"Hello, preferences file\"," + << "\"properties\":{\"persistent\":true}}," << std::endl; + preferencesFile << "{\"type\":\"int\"," + << "\"name\":\"/Preferences/testFileGetInt\"," + << "\"value\":1," + << "\"properties\":{\"persistent\":true}}," << std::endl; + preferencesFile << "{\"type\":\"double\"," + << "\"name\":\"/Preferences/testFileGetDouble\"," + << "\"value\":0.5," + << "\"properties\":{\"persistent\":true}}," << std::endl; + preferencesFile << "{\"type\":\"float\"," + << "\"name\":\"/Preferences/testFileGetFloat\"," + << "\"value\":0.25," + << "\"properties\":{\"persistent\":true}}," << std::endl; + preferencesFile << "{\"type\":\"boolean\"," + << "\"name\":\"/Preferences/testFileGetBoolean\"," + << "\"value\":true," + << "\"properties\":{\"persistent\":true}}]" << std::endl; preferencesFile.close(); + nt::MultiSubscriber suball{inst, {{std::string_view{}}}}; inst.StartServer(); + int count = 0; + while ((inst.GetNetworkMode() & NT_NET_MODE_STARTING) != 0) { + frc::Wait(10_ms); + count++; + if (count > 30) { + FAIL() << "timed out waiting for server startup"; + } + } + EXPECT_EQ("Hello, preferences file", frc::Preferences::GetString("testFileGetString")); EXPECT_EQ(1, frc::Preferences::GetInt("testFileGetInt")); EXPECT_FLOAT_EQ(0.5, frc::Preferences::GetDouble("testFileGetDouble")); EXPECT_FLOAT_EQ(0.25f, frc::Preferences::GetFloat("testFileGetFloat")); EXPECT_TRUE(frc::Preferences::GetBoolean("testFileGetBoolean")); - EXPECT_EQ(1000000000000000000ll, - frc::Preferences::GetLong("testFileGetLong")); } /** * If we set some values using the Preferences class, test that they show up - * in networktables.ini + * in networktables.json */ TEST(PreferencesTest, WritePreferencesToFile) { auto inst = nt::NetworkTableInstance::GetDefault(); inst.StartServer(); + + int count = 0; + while ((inst.GetNetworkMode() & NT_NET_MODE_STARTING) != 0) { + frc::Wait(10_ms); + count++; + if (count > 30) { + FAIL() << "timed out waiting for server startup"; + } + } + frc::Preferences::Remove("testFileGetString"); frc::Preferences::Remove("testFileGetInt"); frc::Preferences::Remove("testFileGetDouble"); frc::Preferences::Remove("testFileGetFloat"); frc::Preferences::Remove("testFileGetBoolean"); - frc::Preferences::Remove("testFileGetLong"); frc::Wait(kSaveTime); @@ -76,19 +101,52 @@ TEST(PreferencesTest, WritePreferencesToFile) { frc::Preferences::SetDouble("testFileSetDouble", 0.5); frc::Preferences::SetFloat("testFileSetFloat", 0.25f); frc::Preferences::SetBoolean("testFileSetBoolean", true); - frc::Preferences::SetLong("testFileSetLong", 1000000000000000000ll); frc::Wait(kSaveTime); static char const* kExpectedFileContents[] = { - "[NetworkTables Storage 3.0]", - "string \"/Preferences/.type\"=\"RobotPreferences\"", - "boolean \"/Preferences/testFileSetBoolean\"=true", - "double \"/Preferences/testFileSetDouble\"=0.5", - "double \"/Preferences/testFileSetFloat\"=0.25", - "double \"/Preferences/testFileSetInt\"=1", - "double \"/Preferences/testFileSetLong\"=1e+18", - "string \"/Preferences/testFileSetString\"=\"Hello, preferences file\""}; + "[", + " {", + " \"name\": \"/Preferences/testFileSetString\",", + " \"type\": \"string\",", + " \"value\": \"Hello, preferences file\",", + " \"properties\": {", + " \"persistent\": true", + " }", + " },", + " {", + " \"name\": \"/Preferences/testFileSetInt\",", + " \"type\": \"int\",", + " \"value\": 1,", + " \"properties\": {", + " \"persistent\": true", + " }", + " },", + " {", + " \"name\": \"/Preferences/testFileSetDouble\",", + " \"type\": \"double\",", + " \"value\": 0.5,", + " \"properties\": {", + " \"persistent\": true", + " }", + " },", + " {", + " \"name\": \"/Preferences/testFileSetFloat\",", + " \"type\": \"float\",", + " \"value\": 0.25,", + " \"properties\": {", + " \"persistent\": true", + " }", + " },", + " {", + " \"name\": \"/Preferences/testFileSetBoolean\",", + " \"type\": \"boolean\",", + " \"value\": true,", + " \"properties\": {", + " \"persistent\": true", + " }", + " }", + "]"}; std::ifstream preferencesFile(kFileName); for (auto& kExpectedFileContent : kExpectedFileContents) { @@ -99,6 +157,6 @@ TEST(PreferencesTest, WritePreferencesToFile) { std::getline(preferencesFile, line); ASSERT_EQ(kExpectedFileContent, line) - << "A line in networktables.ini was not correct"; + << "A line in networktables.json was not correct"; } } diff --git a/wpilibj/build.gradle b/wpilibj/build.gradle index 1e0c45f5eb..77eb349b73 100644 --- a/wpilibj/build.gradle +++ b/wpilibj/build.gradle @@ -112,12 +112,12 @@ model { } } binaries.all { - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(it, 'shared') + project(':ntcore').addNtcoreJniDependency(it) lib project: ':cscore', library: 'cscore', linkage: 'shared' lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' - lib project: ':ntcore', library: 'ntcoreJNIShared', linkage: 'shared' lib project: ':cscore', library: 'cscoreJNIShared', linkage: 'shared' lib project: ':wpinet', library: 'wpinetJNIShared', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutilJNIShared', linkage: 'shared' diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java index 13fe85c46c..fddc01d51f 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_I2C.java @@ -10,9 +10,10 @@ import edu.wpi.first.hal.HAL; import edu.wpi.first.hal.SimDevice; import edu.wpi.first.hal.SimDouble; import edu.wpi.first.hal.SimEnum; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.DoubleTopic; import edu.wpi.first.networktables.NTSendable; import edu.wpi.first.networktables.NTSendableBuilder; -import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.util.sendable.SendableRegistry; import edu.wpi.first.wpilibj.interfaces.Accelerometer; import java.nio.ByteBuffer; @@ -231,15 +232,18 @@ public class ADXL345_I2C implements Accelerometer, NTSendable, AutoCloseable { @Override public void initSendable(NTSendableBuilder builder) { builder.setSmartDashboardType("3AxisAccelerometer"); - NetworkTableEntry entryX = builder.getEntry("X"); - NetworkTableEntry entryY = builder.getEntry("Y"); - NetworkTableEntry entryZ = builder.getEntry("Z"); + DoublePublisher pubX = new DoubleTopic(builder.getTopic("X")).publish(); + DoublePublisher pubY = new DoubleTopic(builder.getTopic("Y")).publish(); + DoublePublisher pubZ = new DoubleTopic(builder.getTopic("Z")).publish(); + builder.addCloseable(pubX); + builder.addCloseable(pubY); + builder.addCloseable(pubZ); builder.setUpdateTable( () -> { AllAxes data = getAccelerations(); - entryX.setDouble(data.XAxis); - entryY.setDouble(data.YAxis); - entryZ.setDouble(data.ZAxis); + pubX.set(data.XAxis); + pubY.set(data.YAxis); + pubZ.set(data.ZAxis); }); } } diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java index ddf38e1c5c..4806979983 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL345_SPI.java @@ -10,9 +10,10 @@ import edu.wpi.first.hal.HAL; import edu.wpi.first.hal.SimDevice; import edu.wpi.first.hal.SimDouble; import edu.wpi.first.hal.SimEnum; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.DoubleTopic; import edu.wpi.first.networktables.NTSendable; import edu.wpi.first.networktables.NTSendableBuilder; -import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.util.sendable.SendableRegistry; import edu.wpi.first.wpilibj.interfaces.Accelerometer; import java.nio.ByteBuffer; @@ -234,15 +235,18 @@ public class ADXL345_SPI implements Accelerometer, NTSendable, AutoCloseable { @Override public void initSendable(NTSendableBuilder builder) { builder.setSmartDashboardType("3AxisAccelerometer"); - NetworkTableEntry entryX = builder.getEntry("X"); - NetworkTableEntry entryY = builder.getEntry("Y"); - NetworkTableEntry entryZ = builder.getEntry("Z"); + DoublePublisher pubX = new DoubleTopic(builder.getTopic("X")).publish(); + DoublePublisher pubY = new DoubleTopic(builder.getTopic("Y")).publish(); + DoublePublisher pubZ = new DoubleTopic(builder.getTopic("Z")).publish(); + builder.addCloseable(pubX); + builder.addCloseable(pubY); + builder.addCloseable(pubZ); builder.setUpdateTable( () -> { AllAxes data = getAccelerations(); - entryX.setDouble(data.XAxis); - entryY.setDouble(data.YAxis); - entryZ.setDouble(data.ZAxis); + pubX.set(data.XAxis); + pubY.set(data.YAxis); + pubZ.set(data.ZAxis); }); } } diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java index 19abf05a50..afd76d6656 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/ADXL362.java @@ -9,9 +9,10 @@ import edu.wpi.first.hal.HAL; import edu.wpi.first.hal.SimDevice; import edu.wpi.first.hal.SimDouble; import edu.wpi.first.hal.SimEnum; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.DoubleTopic; import edu.wpi.first.networktables.NTSendable; import edu.wpi.first.networktables.NTSendableBuilder; -import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.util.sendable.SendableRegistry; import edu.wpi.first.wpilibj.interfaces.Accelerometer; import java.nio.ByteBuffer; @@ -263,15 +264,18 @@ public class ADXL362 implements Accelerometer, NTSendable, AutoCloseable { @Override public void initSendable(NTSendableBuilder builder) { builder.setSmartDashboardType("3AxisAccelerometer"); - NetworkTableEntry entryX = builder.getEntry("X"); - NetworkTableEntry entryY = builder.getEntry("Y"); - NetworkTableEntry entryZ = builder.getEntry("Z"); + DoublePublisher pubX = new DoubleTopic(builder.getTopic("X")).publish(); + DoublePublisher pubY = new DoubleTopic(builder.getTopic("Y")).publish(); + DoublePublisher pubZ = new DoubleTopic(builder.getTopic("Z")).publish(); + builder.addCloseable(pubX); + builder.addCloseable(pubY); + builder.addCloseable(pubZ); builder.setUpdateTable( () -> { AllAxes data = getAccelerations(); - entryX.setDouble(data.XAxis); - entryY.setDouble(data.YAxis); - entryZ.setDouble(data.ZAxis); + pubX.set(data.XAxis); + pubY.set(data.YAxis); + pubZ.set(data.ZAxis); }); } } diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java index be3c20717e..ce8a76c88a 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/DriverStation.java @@ -8,9 +8,11 @@ import edu.wpi.first.hal.AllianceStationID; import edu.wpi.first.hal.ControlWord; import edu.wpi.first.hal.HAL; import edu.wpi.first.hal.MatchInfoData; +import edu.wpi.first.networktables.BooleanPublisher; +import edu.wpi.first.networktables.IntegerPublisher; import edu.wpi.first.networktables.NetworkTable; -import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.StringPublisher; import edu.wpi.first.util.WPIUtilJNI; import edu.wpi.first.util.datalog.BooleanArrayLogEntry; import edu.wpi.first.util.datalog.BooleanLogEntry; @@ -83,15 +85,15 @@ public final class DriverStation { @SuppressWarnings("MemberName") private static class MatchDataSender { NetworkTable table; - NetworkTableEntry typeMetadata; - NetworkTableEntry gameSpecificMessage; - NetworkTableEntry eventName; - NetworkTableEntry matchNumber; - NetworkTableEntry replayNumber; - NetworkTableEntry matchType; - NetworkTableEntry alliance; - NetworkTableEntry station; - NetworkTableEntry controlWord; + StringPublisher typeMetadata; + StringPublisher gameSpecificMessage; + StringPublisher eventName; + IntegerPublisher matchNumber; + IntegerPublisher replayNumber; + IntegerPublisher matchType; + BooleanPublisher alliance; + IntegerPublisher station; + IntegerPublisher controlWord; boolean oldIsRedAlliance = true; int oldStationNumber = 1; String oldEventName = ""; @@ -103,24 +105,24 @@ public final class DriverStation { MatchDataSender() { table = NetworkTableInstance.getDefault().getTable("FMSInfo"); - typeMetadata = table.getEntry(".type"); - typeMetadata.forceSetString("FMSInfo"); - gameSpecificMessage = table.getEntry("GameSpecificMessage"); - gameSpecificMessage.forceSetString(""); - eventName = table.getEntry("EventName"); - eventName.forceSetString(""); - matchNumber = table.getEntry("MatchNumber"); - matchNumber.forceSetDouble(0); - replayNumber = table.getEntry("ReplayNumber"); - replayNumber.forceSetDouble(0); - matchType = table.getEntry("MatchType"); - matchType.forceSetDouble(0); - alliance = table.getEntry("IsRedAlliance"); - alliance.forceSetBoolean(true); - station = table.getEntry("StationNumber"); - station.forceSetDouble(1); - controlWord = table.getEntry("FMSControlData"); - controlWord.forceSetDouble(0); + typeMetadata = table.getStringTopic(".type").publish(); + typeMetadata.set("FMSInfo"); + gameSpecificMessage = table.getStringTopic("GameSpecificMessage").publish(); + gameSpecificMessage.set(""); + eventName = table.getStringTopic("EventName").publish(); + eventName.set(""); + matchNumber = table.getIntegerTopic("MatchNumber").publish(); + matchNumber.set(0); + replayNumber = table.getIntegerTopic("ReplayNumber").publish(); + replayNumber.set(0); + matchType = table.getIntegerTopic("MatchType").publish(); + matchType.set(0); + alliance = table.getBooleanTopic("IsRedAlliance").publish(); + alliance.set(true); + station = table.getIntegerTopic("StationNumber").publish(); + station.set(1); + controlWord = table.getIntegerTopic("FMSControlData").publish(); + controlWord.set(0); } private void sendMatchData() { @@ -173,35 +175,35 @@ public final class DriverStation { currentControlWord = HAL.nativeGetControlWord(); if (oldIsRedAlliance != isRedAlliance) { - alliance.setBoolean(isRedAlliance); + alliance.set(isRedAlliance); oldIsRedAlliance = isRedAlliance; } if (oldStationNumber != stationNumber) { - station.setDouble(stationNumber); + station.set(stationNumber); oldStationNumber = stationNumber; } if (!oldEventName.equals(currentEventName)) { - eventName.setString(currentEventName); + eventName.set(currentEventName); oldEventName = currentEventName; } if (!oldGameSpecificMessage.equals(currentGameSpecificMessage)) { - gameSpecificMessage.setString(currentGameSpecificMessage); + gameSpecificMessage.set(currentGameSpecificMessage); oldGameSpecificMessage = currentGameSpecificMessage; } if (currentMatchNumber != oldMatchNumber) { - matchNumber.setDouble(currentMatchNumber); + matchNumber.set(currentMatchNumber); oldMatchNumber = currentMatchNumber; } if (currentReplayNumber != oldReplayNumber) { - replayNumber.setDouble(currentReplayNumber); + replayNumber.set(currentReplayNumber); oldReplayNumber = currentReplayNumber; } if (currentMatchType != oldMatchType) { - matchType.setDouble(currentMatchType); + matchType.set(currentMatchType); oldMatchType = currentMatchType; } if (currentControlWord != oldControlWord) { - controlWord.setDouble(currentControlWord); + controlWord.set(currentControlWord); oldControlWord = currentControlWord; } } diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java index d314830bcf..19f81e15b5 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/IterativeRobotBase.java @@ -65,7 +65,7 @@ public abstract class IterativeRobotBase extends RobotBase { private Mode m_lastMode = Mode.kNone; private final double m_period; private final Watchdog m_watchdog; - private boolean m_ntFlushEnabled; + private boolean m_ntFlushEnabled = true; /** * Constructor for IterativeRobotBase. @@ -235,7 +235,7 @@ public abstract class IterativeRobotBase extends RobotBase { public void testExit() {} /** - * Enables or disables flushing NetworkTables every loop iteration. By default, this is disabled. + * Enables or disables flushing NetworkTables every loop iteration. By default, this is enabled. * * @param enabled True to enable, false to disable */ @@ -343,7 +343,7 @@ public abstract class IterativeRobotBase extends RobotBase { // Flush NetworkTables if (m_ntFlushEnabled) { - NetworkTableInstance.getDefault().flush(); + NetworkTableInstance.getDefault().flushLocal(); } // Warn on loop time overruns diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java index cd4675c479..6b9a952705 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java @@ -8,10 +8,13 @@ import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam; import edu.wpi.first.hal.FRCNetComm.tResourceType; import edu.wpi.first.hal.HAL; -import edu.wpi.first.networktables.EntryListenerFlags; +import edu.wpi.first.networktables.MultiSubscriber; import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.StringPublisher; +import edu.wpi.first.networktables.TopicListener; +import edu.wpi.first.networktables.TopicListenerFlags; import java.util.Collection; /** @@ -31,20 +34,51 @@ public final class Preferences { /** The Preferences table name. */ private static final String TABLE_NAME = "Preferences"; /** The network table. */ - private static final NetworkTable m_table; + private static NetworkTable m_table; + + private static StringPublisher m_typePublisher; + private static MultiSubscriber m_tableSubscriber; + private static TopicListener m_listener; /** Creates a preference class. */ private Preferences() {} static { - m_table = NetworkTableInstance.getDefault().getTable(TABLE_NAME); - m_table.getEntry(".type").setString("RobotPreferences"); + setNetworkTableInstance(NetworkTableInstance.getDefault()); + HAL.report(tResourceType.kResourceType_Preferences, 0); + } + + /** + * Set the NetworkTable instance used for entries. For testing purposes; use with caution. + * + * @param inst NetworkTable instance + */ + public static synchronized void setNetworkTableInstance(NetworkTableInstance inst) { + m_table = inst.getTable(TABLE_NAME); + if (m_typePublisher != null) { + m_typePublisher.close(); + } + m_typePublisher = m_table.getStringTopic(".type").publish(); + m_typePublisher.set("RobotPreferences"); + + // Subscribe to all Preferences; this ensures we get the latest values + // ahead of a getter call. + if (m_tableSubscriber != null) { + m_tableSubscriber.close(); + } + m_tableSubscriber = new MultiSubscriber(inst, new String[] {m_table.getPath() + "/"}); + // Listener to set all Preferences values to persistent // (for backwards compatibility with old dashboards). - m_table.addEntryListener( - (table, key, entry, value, flags) -> entry.setPersistent(), - EntryListenerFlags.kImmediate | EntryListenerFlags.kNew); - HAL.report(tResourceType.kResourceType_Preferences, 0); + if (m_listener != null) { + m_listener.close(); + } + m_listener = + new TopicListener( + m_table.getInstance(), + new String[] {m_table.getPath() + "/"}, + TopicListenerFlags.kImmediate | TopicListenerFlags.kPublish, + event -> event.info.getTopic().setPersistent(true)); } /** @@ -219,7 +253,9 @@ public final class Preferences { * @param key the key */ public static void remove(String key) { - m_table.delete(key); + NetworkTableEntry entry = m_table.getEntry(key); + entry.clearPersistent(); + entry.unpublish(); } /** Remove all preferences. */ diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java index 78e4d46feb..07b0806248 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/RobotBase.java @@ -14,6 +14,7 @@ import edu.wpi.first.hal.HALUtil; import edu.wpi.first.math.MathShared; import edu.wpi.first.math.MathSharedStore; import edu.wpi.first.math.MathUsageId; +import edu.wpi.first.networktables.MultiSubscriber; import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.wpilibj.livewindow.LiveWindow; import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard; @@ -38,6 +39,8 @@ public abstract class RobotBase implements AutoCloseable { // This is usually 1, but it is best to make sure private static long m_threadId = -1; + private final MultiSubscriber m_suball; + private static void setupCameraServerShared() { CameraServerShared shared = new CameraServerShared() { @@ -142,12 +145,27 @@ public abstract class RobotBase implements AutoCloseable { setupCameraServerShared(); setupMathShared(); inst.setNetworkIdentity("Robot"); + // subscribe to "" to force persistent values to progagate to local + m_suball = new MultiSubscriber(inst, new String[] {""}); if (isReal()) { - inst.startServer("/home/lvuser/networktables.ini"); + inst.startServer("/home/lvuser/networktables.json"); } else { inst.startServer(); } - inst.getTable("LiveWindow").getSubTable(".status").getEntry("LW Enabled").setBoolean(false); + + // wait for the NT server to actually start + try { + int count = 0; + while ((inst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) { + Thread.sleep(10); + count++; + if (count > 100) { + throw new InterruptedException(); + } + } + } catch (InterruptedException ex) { + System.err.println("timed out while waiting for NT server to start"); + } LiveWindow.setEnabled(false); Shuffleboard.disableActuatorWidgets(); @@ -158,7 +176,9 @@ public abstract class RobotBase implements AutoCloseable { } @Override - public void close() {} + public void close() { + m_suball.close(); + } /** * Get the current runtime type. diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/NetworkBooleanEvent.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/NetworkBooleanEvent.java new file mode 100644 index 0000000000..53fa451e24 --- /dev/null +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/event/NetworkBooleanEvent.java @@ -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. + +package edu.wpi.first.wpilibj.event; + +import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam; + +import edu.wpi.first.networktables.BooleanSubscriber; +import edu.wpi.first.networktables.BooleanTopic; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; + +/** This class provides an easy way to link NetworkTables boolean topics to callback actions. */ +public class NetworkBooleanEvent extends BooleanEvent { + /** + * Creates a new event with the given boolean topic determining whether it is active. + * + * @param loop the loop that polls this event + * @param topic The boolean topic that contains the value + */ + public NetworkBooleanEvent(EventLoop loop, BooleanTopic topic) { + this(loop, topic.subscribe(false)); + } + + /** + * Creates a new event with the given boolean subscriber determining whether it is active. + * + * @param loop the loop that polls this event + * @param sub The boolean subscriber that provides the value + */ + public NetworkBooleanEvent(EventLoop loop, BooleanSubscriber sub) { + super(loop, () -> sub.getTopic().getInstance().isConnected() && sub.get()); + requireNonNullParam(sub, "sub", "NetworkBooleanEvent"); + } + + /** + * Creates a new event with the given boolean topic determining whether it is active. + * + * @param loop the loop that polls this event + * @param table The NetworkTable that contains the topic + * @param topicName The topic name within the table that contains the value + */ + public NetworkBooleanEvent(EventLoop loop, NetworkTable table, String topicName) { + this(loop, table.getBooleanTopic(topicName)); + } + + /** + * Creates a new event with the given boolean topic determining whether it is active. + * + * @param loop the loop that polls this event + * @param tableName The NetworkTable name that contains the topic + * @param topicName The topic name within the table that contains the value + */ + public NetworkBooleanEvent(EventLoop loop, String tableName, String topicName) { + this(loop, NetworkTableInstance.getDefault(), tableName, topicName); + } + + /** + * Creates a new event with the given boolean topic determining whether it is active. + * + * @param loop the loop that polls this event + * @param inst The NetworkTable instance to use + * @param tableName The NetworkTable that contains the topic + * @param topicName The topic name within the table that contains the value + */ + public NetworkBooleanEvent( + EventLoop loop, NetworkTableInstance inst, String tableName, String topicName) { + this(loop, inst.getTable(tableName), topicName); + } +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java index 859b38c486..b775f327b5 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/livewindow/LiveWindow.java @@ -4,9 +4,11 @@ package edu.wpi.first.wpilibj.livewindow; +import edu.wpi.first.networktables.BooleanPublisher; import edu.wpi.first.networktables.NetworkTable; -import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.StringPublisher; +import edu.wpi.first.networktables.StringTopic; import edu.wpi.first.util.sendable.Sendable; import edu.wpi.first.util.sendable.SendableRegistry; import edu.wpi.first.wpilibj.smartdashboard.SendableBuilderImpl; @@ -15,16 +17,31 @@ import edu.wpi.first.wpilibj.smartdashboard.SendableBuilderImpl; * The LiveWindow class is the public interface for putting sensors and actuators on the LiveWindow. */ public final class LiveWindow { - private static class Component { + private static class Component implements AutoCloseable { + @Override + public void close() { + if (m_namePub != null) { + m_namePub.close(); + m_namePub = null; + } + if (m_typePub != null) { + m_typePub.close(); + m_typePub = null; + } + } + boolean m_firstTime = true; boolean m_telemetryEnabled; + StringPublisher m_namePub; + StringPublisher m_typePub; } private static final int dataHandle = SendableRegistry.getDataHandle(); private static final NetworkTable liveWindowTable = NetworkTableInstance.getDefault().getTable("LiveWindow"); private static final NetworkTable statusTable = liveWindowTable.getSubTable(".status"); - private static final NetworkTableEntry enabledEntry = statusTable.getEntry("LW Enabled"); + private static final BooleanPublisher enabledPub = + statusTable.getBooleanTopic("LW Enabled").publish(); private static boolean startLiveWindow; private static boolean liveWindowEnabled; private static boolean telemetryEnabled; @@ -34,6 +51,7 @@ public final class LiveWindow { static { SendableRegistry.setLiveWindowBuilderFactory(() -> new SendableBuilderImpl()); + enabledPub.set(false); } private static Component getOrAdd(Sendable sendable) { @@ -95,7 +113,7 @@ public final class LiveWindow { disabledListener.run(); } } - enabledEntry.setBoolean(enabled); + enabledPub.set(enabled); } } @@ -177,10 +195,12 @@ public final class LiveWindow { } else { table = ssTable.getSubTable(cbdata.name); } - table.getEntry(".name").setString(cbdata.name); + component.m_namePub = new StringTopic(table.getTopic(".name")).publish(); + component.m_namePub.set(cbdata.name); ((SendableBuilderImpl) cbdata.builder).setTable(table); cbdata.sendable.initSendable(cbdata.builder); - ssTable.getEntry(".type").setString("LW Subsystem"); + component.m_typePub = new StringTopic(ssTable.getTopic(".type")).publish(); + component.m_typePub.set("LW Subsystem"); component.m_firstTime = false; } diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java index 5f1074e98f..342229cb21 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/BuiltInWidgets.java @@ -12,7 +12,7 @@ import edu.wpi.first.wpilibj.interfaces.Accelerometer.Range; *

For example, setting a number to be displayed with a slider: * *

{@code
- * NetworkTableEntry example = Shuffleboard.getTab("My Tab")
+ * GenericEntry example = Shuffleboard.getTab("My Tab")
  *   .add("My Number", 0)
  *   .withWidget(BuiltInWidgets.kNumberSlider)
  *   .withProperties(Map.of("min", 0, "max", 1))
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java
index 4bfe76c69e..d939059567 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ContainerHelper.java
@@ -4,7 +4,10 @@
 
 package edu.wpi.first.wpilibj.shuffleboard;
 
+import edu.wpi.first.networktables.GenericPublisher;
 import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.networktables.NetworkTableType;
+import edu.wpi.first.util.function.FloatSupplier;
 import edu.wpi.first.util.sendable.Sendable;
 import edu.wpi.first.util.sendable.SendableRegistry;
 import java.util.ArrayList;
@@ -18,6 +21,7 @@ import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BooleanSupplier;
 import java.util.function.DoubleSupplier;
+import java.util.function.LongSupplier;
 import java.util.function.Supplier;
 
 /** A helper class for Shuffleboard containers to handle common child operations. */
@@ -68,6 +72,11 @@ final class ContainerHelper {
   }
 
   SimpleWidget add(String title, Object defaultValue) {
+    Objects.requireNonNull(defaultValue, "Default value cannot be null");
+    return add(title, NetworkTableType.getStringFromObject(defaultValue), defaultValue);
+  }
+
+  SimpleWidget add(String title, String typeString, Object defaultValue) {
     Objects.requireNonNull(title, "Title cannot be null");
     Objects.requireNonNull(defaultValue, "Default value cannot be null");
     checkTitle(title);
@@ -75,43 +84,72 @@ final class ContainerHelper {
 
     SimpleWidget widget = new SimpleWidget(m_container, title);
     m_components.add(widget);
-    widget.getEntry().setDefaultValue(defaultValue);
+    widget.getEntry(typeString).setDefaultValue(defaultValue);
     return widget;
   }
 
   SuppliedValueWidget addString(String title, Supplier valueSupplier) {
     precheck(title, valueSupplier);
-    return addSupplied(title, valueSupplier, NetworkTableEntry::setString);
+    return addSupplied(title, "string", valueSupplier, GenericPublisher::setString);
   }
 
   SuppliedValueWidget addNumber(String title, DoubleSupplier valueSupplier) {
+    return addDouble(title, valueSupplier);
+  }
+
+  SuppliedValueWidget addDouble(String title, DoubleSupplier valueSupplier) {
     precheck(title, valueSupplier);
-    return addSupplied(title, valueSupplier::getAsDouble, NetworkTableEntry::setDouble);
+    return addSupplied(title, "double", valueSupplier::getAsDouble, GenericPublisher::setDouble);
+  }
+
+  SuppliedValueWidget addFloat(String title, FloatSupplier valueSupplier) {
+    precheck(title, valueSupplier);
+    return addSupplied(title, "float", valueSupplier::getAsFloat, GenericPublisher::setFloat);
+  }
+
+  SuppliedValueWidget addInteger(String title, LongSupplier valueSupplier) {
+    precheck(title, valueSupplier);
+    return addSupplied(title, "int", valueSupplier::getAsLong, GenericPublisher::setInteger);
   }
 
   SuppliedValueWidget addBoolean(String title, BooleanSupplier valueSupplier) {
     precheck(title, valueSupplier);
-    return addSupplied(title, valueSupplier::getAsBoolean, NetworkTableEntry::setBoolean);
+    return addSupplied(title, "boolean", valueSupplier::getAsBoolean, GenericPublisher::setBoolean);
   }
 
   SuppliedValueWidget addStringArray(String title, Supplier valueSupplier) {
     precheck(title, valueSupplier);
-    return addSupplied(title, valueSupplier, NetworkTableEntry::setStringArray);
+    return addSupplied(title, "string[]", valueSupplier, GenericPublisher::setStringArray);
   }
 
   SuppliedValueWidget addDoubleArray(String title, Supplier valueSupplier) {
     precheck(title, valueSupplier);
-    return addSupplied(title, valueSupplier, NetworkTableEntry::setDoubleArray);
+    return addSupplied(title, "double[]", valueSupplier, GenericPublisher::setDoubleArray);
+  }
+
+  SuppliedValueWidget addFloatArray(String title, Supplier valueSupplier) {
+    precheck(title, valueSupplier);
+    return addSupplied(title, "float[]", valueSupplier, GenericPublisher::setFloatArray);
+  }
+
+  SuppliedValueWidget addIntegerArray(String title, Supplier valueSupplier) {
+    precheck(title, valueSupplier);
+    return addSupplied(title, "int[]", valueSupplier, GenericPublisher::setIntegerArray);
   }
 
   SuppliedValueWidget addBooleanArray(String title, Supplier valueSupplier) {
     precheck(title, valueSupplier);
-    return addSupplied(title, valueSupplier, NetworkTableEntry::setBooleanArray);
+    return addSupplied(title, "boolean[]", valueSupplier, GenericPublisher::setBooleanArray);
   }
 
   SuppliedValueWidget addRaw(String title, Supplier valueSupplier) {
+    return addRaw(title, "raw", valueSupplier);
+  }
+
+  SuppliedValueWidget addRaw(
+      String title, String typeString, Supplier valueSupplier) {
     precheck(title, valueSupplier);
-    return addSupplied(title, valueSupplier, NetworkTableEntry::setRaw);
+    return addSupplied(title, typeString, valueSupplier, GenericPublisher::setRaw);
   }
 
   private void precheck(String title, Object valueSupplier) {
@@ -121,8 +159,12 @@ final class ContainerHelper {
   }
 
   private  SuppliedValueWidget addSupplied(
-      String title, Supplier supplier, BiConsumer setter) {
-    SuppliedValueWidget widget = new SuppliedValueWidget<>(m_container, title, supplier, setter);
+      String title,
+      String typeString,
+      Supplier supplier,
+      BiConsumer setter) {
+    SuppliedValueWidget widget =
+        new SuppliedValueWidget<>(m_container, title, typeString, supplier, setter);
     m_components.add(widget);
     return widget;
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java
index a7be3c9213..f8329ddbc1 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/RecordingController.java
@@ -4,9 +4,10 @@
 
 package edu.wpi.first.wpilibj.shuffleboard;
 
+import edu.wpi.first.networktables.BooleanPublisher;
 import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
 import edu.wpi.first.networktables.NetworkTableInstance;
+import edu.wpi.first.networktables.StringPublisher;
 import edu.wpi.first.wpilibj.DriverStation;
 
 /** Controls Shuffleboard recordings via NetworkTables. */
@@ -16,30 +17,31 @@ final class RecordingController {
   private static final String kRecordingFileNameFormatKey = kRecordingTableName + "FileNameFormat";
   private static final String kEventMarkerTableName = kRecordingTableName + "events";
 
-  private final NetworkTableEntry m_recordingControlEntry;
-  private final NetworkTableEntry m_recordingFileNameFormatEntry;
+  private final BooleanPublisher m_recordingControlEntry;
+  private final StringPublisher m_recordingFileNameFormatEntry;
   private final NetworkTable m_eventsTable;
 
   RecordingController(NetworkTableInstance ntInstance) {
-    m_recordingControlEntry = ntInstance.getEntry(kRecordingControlKey);
-    m_recordingFileNameFormatEntry = ntInstance.getEntry(kRecordingFileNameFormatKey);
+    m_recordingControlEntry = ntInstance.getBooleanTopic(kRecordingControlKey).publish();
+    m_recordingFileNameFormatEntry =
+        ntInstance.getStringTopic(kRecordingFileNameFormatKey).publish();
     m_eventsTable = ntInstance.getTable(kEventMarkerTableName);
   }
 
   public void startRecording() {
-    m_recordingControlEntry.setBoolean(true);
+    m_recordingControlEntry.set(true);
   }
 
   public void stopRecording() {
-    m_recordingControlEntry.setBoolean(false);
+    m_recordingControlEntry.set(false);
   }
 
   public void setRecordingFileNameFormat(String format) {
-    m_recordingFileNameFormatEntry.setString(format);
+    m_recordingFileNameFormatEntry.set(format);
   }
 
   public void clearRecordingFileNameFormat() {
-    m_recordingFileNameFormatEntry.delete();
+    m_recordingFileNameFormatEntry.set("");
   }
 
   public void addEventMarker(String name, String description, EventImportance importance) {
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java
index 8651815b88..1a7b496e30 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapper.java
@@ -5,7 +5,10 @@
 package edu.wpi.first.wpilibj.shuffleboard;
 
 import edu.wpi.first.cscore.VideoSource;
+import edu.wpi.first.networktables.NetworkTable;
 import edu.wpi.first.networktables.NetworkTableInstance;
+import edu.wpi.first.networktables.StringArrayPublisher;
+import edu.wpi.first.networktables.StringArrayTopic;
 import edu.wpi.first.util.sendable.Sendable;
 import edu.wpi.first.util.sendable.SendableBuilder;
 import edu.wpi.first.util.sendable.SendableRegistry;
@@ -19,7 +22,14 @@ public final class SendableCameraWrapper implements Sendable, AutoCloseable {
 
   private static Map m_wrappers = new WeakHashMap<>();
 
+  private static NetworkTable m_table;
+
+  static {
+    setNetworkTableInstance(NetworkTableInstance.getDefault());
+  }
+
   private final String m_uri;
+  private StringArrayPublisher m_streams;
 
   /**
    * Creates a new sendable wrapper. Private constructor to avoid direct instantiation with multiple
@@ -36,6 +46,19 @@ public final class SendableCameraWrapper implements Sendable, AutoCloseable {
     m_uri = kProtocol + cameraName;
   }
 
+  private SendableCameraWrapper(String cameraName, String[] cameraUrls) {
+    this(cameraName);
+
+    StringArrayTopic streams = new StringArrayTopic(m_table.getTopic(cameraName + "/streams"));
+    if (streams.exists()) {
+      throw new IllegalStateException(
+          "A camera is already being streamed with the name '" + cameraName + "'");
+    }
+
+    m_streams = streams.publish();
+    m_streams.set(cameraUrls);
+  }
+
   /** Clears all cached wrapper objects. This should only be used in tests. */
   static void clearWrappers() {
     m_wrappers.clear();
@@ -44,6 +67,18 @@ public final class SendableCameraWrapper implements Sendable, AutoCloseable {
   @Override
   public void close() {
     SendableRegistry.remove(this);
+    if (m_streams != null) {
+      m_streams.close();
+    }
+  }
+
+  /*
+   * Sets NetworkTable instance used for camera publisher entries.
+   *
+   * @param inst NetworkTable instance
+   */
+  public static synchronized void setNetworkTableInstance(NetworkTableInstance inst) {
+    m_table = inst.getTable("CameraPublisher");
   }
 
   /**
@@ -89,15 +124,7 @@ public final class SendableCameraWrapper implements Sendable, AutoCloseable {
       Objects.requireNonNull(cameraUrls[i], "Camera URL at index " + i + " was null");
     }
 
-    String streams = "/CameraPublisher/" + cameraName + "/streams";
-    if (NetworkTableInstance.getDefault().getEntries(streams, 0).length != 0) {
-      throw new IllegalStateException(
-          "A camera is already being streamed with the name '" + cameraName + "'");
-    }
-
-    NetworkTableInstance.getDefault().getEntry(streams).setStringArray(cameraUrls);
-
-    SendableCameraWrapper wrapper = new SendableCameraWrapper(cameraName);
+    SendableCameraWrapper wrapper = new SendableCameraWrapper(cameraName, cameraUrls);
     m_wrappers.put(cameraName, wrapper);
     return wrapper;
   }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java
index 1bea28ec7f..c08ab6ffeb 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/Shuffleboard.java
@@ -16,7 +16,7 @@ import edu.wpi.first.networktables.NetworkTableInstance;
  * 

For example, displaying a boolean entry with a toggle button: * *

{@code
- * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ * GenericEntry myBoolean = Shuffleboard.getTab("Example Tab")
  *   .add("My Boolean", false)
  *   .withWidget("Toggle Button")
  *   .getEntry();
@@ -25,7 +25,7 @@ import edu.wpi.first.networktables.NetworkTableInstance;
  * 

Changing the colors of the boolean box: * *

{@code
- * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ * GenericEntry myBoolean = Shuffleboard.getTab("Example Tab")
  *   .add("My Boolean", false)
  *   .withWidget("Boolean Box")
  *   .withProperties(Map.of("colorWhenTrue", "green", "colorWhenFalse", "maroon"))
@@ -36,7 +36,7 @@ import edu.wpi.first.networktables.NetworkTableInstance;
  * the layout has already been generated by a previously defined entry.
  *
  * 
{@code
- * NetworkTableEntry myBoolean = Shuffleboard.getTab("Example Tab")
+ * GenericEntry myBoolean = Shuffleboard.getTab("Example Tab")
  *   .getLayout("List", "Example List")
  *   .add("My Boolean", false)
  *   .withWidget("Toggle Button")
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java
index fd7d541fce..a7c1f6cf04 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardComponent.java
@@ -115,21 +115,21 @@ public abstract class ShuffleboardComponent>
     }
     // Component type
     if (getType() == null) {
-      metaTable.getEntry("PreferredComponent").delete();
+      metaTable.getEntry("PreferredComponent").unpublish();
     } else {
-      metaTable.getEntry("PreferredComponent").forceSetString(getType());
+      metaTable.getEntry("PreferredComponent").setString(getType());
     }
 
     // Tile size
     if (m_width <= 0 || m_height <= 0) {
-      metaTable.getEntry("Size").delete();
+      metaTable.getEntry("Size").unpublish();
     } else {
       metaTable.getEntry("Size").setDoubleArray(new double[] {m_width, m_height});
     }
 
     // Tile position
     if (m_column < 0 || m_row < 0) {
-      metaTable.getEntry("Position").delete();
+      metaTable.getEntry("Position").unpublish();
     } else {
       metaTable.getEntry("Position").setDoubleArray(new double[] {m_column, m_row});
     }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java
index 333623fdd5..b19aa2af2d 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardContainer.java
@@ -5,11 +5,14 @@
 package edu.wpi.first.wpilibj.shuffleboard;
 
 import edu.wpi.first.cscore.VideoSource;
+import edu.wpi.first.networktables.NetworkTableType;
+import edu.wpi.first.util.function.FloatSupplier;
 import edu.wpi.first.util.sendable.Sendable;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.function.BooleanSupplier;
 import java.util.function.DoubleSupplier;
+import java.util.function.LongSupplier;
 import java.util.function.Supplier;
 
 /** Common interface for objects that can contain shuffleboard components. */
@@ -122,6 +125,19 @@ public interface ShuffleboardContainer extends ShuffleboardValue {
    */
   SimpleWidget add(String title, Object defaultValue);
 
+  /**
+   * Adds a widget to this container to display the given data.
+   *
+   * @param title the title of the widget
+   * @param typeString the NT type string
+   * @param defaultValue the default value of the widget
+   * @return a widget to display the sendable data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   * @see #addPersistent(String, Object) add(String title, Object defaultValue)
+   */
+  SimpleWidget add(String title, String typeString, Object defaultValue);
+
   /**
    * Adds a widget to this container to display a video stream.
    *
@@ -162,6 +178,45 @@ public interface ShuffleboardContainer extends ShuffleboardValue {
    */
   SuppliedValueWidget addNumber(String title, DoubleSupplier valueSupplier);
 
+  /**
+   * Adds a widget to this container. The widget will display the data provided by the value
+   * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
+   * overridden by values from the value supplier.
+   *
+   * @param title the title of the widget
+   * @param valueSupplier the supplier for values
+   * @return a widget to display data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   */
+  SuppliedValueWidget addDouble(String title, DoubleSupplier valueSupplier);
+
+  /**
+   * Adds a widget to this container. The widget will display the data provided by the value
+   * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
+   * overridden by values from the value supplier.
+   *
+   * @param title the title of the widget
+   * @param valueSupplier the supplier for values
+   * @return a widget to display data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   */
+  SuppliedValueWidget addFloat(String title, FloatSupplier valueSupplier);
+
+  /**
+   * Adds a widget to this container. The widget will display the data provided by the value
+   * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
+   * overridden by values from the value supplier.
+   *
+   * @param title the title of the widget
+   * @param valueSupplier the supplier for values
+   * @return a widget to display data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   */
+  SuppliedValueWidget addInteger(String title, LongSupplier valueSupplier);
+
   /**
    * Adds a widget to this container. The widget will display the data provided by the value
    * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
@@ -201,6 +256,32 @@ public interface ShuffleboardContainer extends ShuffleboardValue {
    */
   SuppliedValueWidget addDoubleArray(String title, Supplier valueSupplier);
 
+  /**
+   * Adds a widget to this container. The widget will display the data provided by the value
+   * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
+   * overridden by values from the value supplier.
+   *
+   * @param title the title of the widget
+   * @param valueSupplier the supplier for values
+   * @return a widget to display data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   */
+  SuppliedValueWidget addFloatArray(String title, Supplier valueSupplier);
+
+  /**
+   * Adds a widget to this container. The widget will display the data provided by the value
+   * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
+   * overridden by values from the value supplier.
+   *
+   * @param title the title of the widget
+   * @param valueSupplier the supplier for values
+   * @return a widget to display data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   */
+  SuppliedValueWidget addIntegerArray(String title, Supplier valueSupplier);
+
   /**
    * Adds a widget to this container. The widget will display the data provided by the value
    * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
@@ -225,7 +306,24 @@ public interface ShuffleboardContainer extends ShuffleboardValue {
    * @throws IllegalArgumentException if a widget already exists in this container with the given
    *     title
    */
-  SuppliedValueWidget addRaw(String title, Supplier valueSupplier);
+  default SuppliedValueWidget addRaw(String title, Supplier valueSupplier) {
+    return addRaw(title, "raw", valueSupplier);
+  }
+
+  /**
+   * Adds a widget to this container. The widget will display the data provided by the value
+   * supplier. Changes made on the dashboard will not propagate to the widget object, and will be
+   * overridden by values from the value supplier.
+   *
+   * @param title the title of the widget
+   * @param typeString the NT type string for the value
+   * @param valueSupplier the supplier for values
+   * @return a widget to display data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   */
+  SuppliedValueWidget addRaw(
+      String title, String typeString, Supplier valueSupplier);
 
   /**
    * Adds a widget to this container to display a simple piece of data. Unlike {@link #add(String,
@@ -240,8 +338,25 @@ public interface ShuffleboardContainer extends ShuffleboardValue {
    * @see #add(String, Object) add(String title, Object defaultValue)
    */
   default SimpleWidget addPersistent(String title, Object defaultValue) {
+    return addPersistent(title, NetworkTableType.getStringFromObject(defaultValue), defaultValue);
+  }
+
+  /**
+   * Adds a widget to this container to display a simple piece of data. Unlike {@link #add(String,
+   * Object)}, the value in the widget will be saved on the robot and will be used when the robot
+   * program next starts rather than {@code defaultValue}.
+   *
+   * @param title the title of the widget
+   * @param typeString the NT type string
+   * @param defaultValue the default value of the widget
+   * @return a widget to display the sendable data
+   * @throws IllegalArgumentException if a widget already exists in this container with the given
+   *     title
+   * @see #add(String, Object) add(String title, Object defaultValue)
+   */
+  default SimpleWidget addPersistent(String title, String typeString, Object defaultValue) {
     SimpleWidget widget = add(title, defaultValue);
-    widget.getEntry().setPersistent();
+    widget.getEntry(typeString).getTopic().setPersistent(true);
     return widget;
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java
index a8644331ad..1d87882c4a 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstance.java
@@ -9,8 +9,8 @@ import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 import edu.wpi.first.hal.FRCNetComm.tResourceType;
 import edu.wpi.first.hal.HAL;
 import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
 import edu.wpi.first.networktables.NetworkTableInstance;
+import edu.wpi.first.networktables.StringPublisher;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.function.Consumer;
@@ -21,7 +21,7 @@ final class ShuffleboardInstance implements ShuffleboardRoot {
   private boolean m_tabsChanged = false; // NOPMD redundant field initializer
   private final NetworkTable m_rootTable;
   private final NetworkTable m_rootMetaTable;
-  private final NetworkTableEntry m_selectedTabEntry;
+  private final StringPublisher m_selectedTabPub;
 
   /**
    * Creates a new Shuffleboard instance.
@@ -32,7 +32,7 @@ final class ShuffleboardInstance implements ShuffleboardRoot {
     requireNonNullParam(ntInstance, "ntInstance", "ShuffleboardInstance");
     m_rootTable = ntInstance.getTable(Shuffleboard.kBaseTableName);
     m_rootMetaTable = m_rootTable.getSubTable(".metadata");
-    m_selectedTabEntry = m_rootMetaTable.getEntry("Selected");
+    m_selectedTabPub = m_rootMetaTable.getStringTopic("Selected").publish();
     HAL.report(tResourceType.kResourceType_Shuffleboard, 0);
   }
 
@@ -51,7 +51,7 @@ final class ShuffleboardInstance implements ShuffleboardRoot {
     if (m_tabsChanged) {
       String[] tabTitles =
           m_tabs.values().stream().map(ShuffleboardTab::getTitle).toArray(String[]::new);
-      m_rootMetaTable.getEntry("Tabs").forceSetStringArray(tabTitles);
+      m_rootMetaTable.getEntry("Tabs").setStringArray(tabTitles);
       m_tabsChanged = false;
     }
     for (ShuffleboardTab tab : m_tabs.values()) {
@@ -72,12 +72,12 @@ final class ShuffleboardInstance implements ShuffleboardRoot {
 
   @Override
   public void selectTab(int index) {
-    m_selectedTabEntry.forceSetDouble(index);
+    selectTab(Integer.toString(index));
   }
 
   @Override
   public void selectTab(String title) {
-    m_selectedTabEntry.forceSetString(title);
+    m_selectedTabPub.set(title);
   }
 
   /**
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java
index b48eebbeb5..4135e090a0 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardLayout.java
@@ -7,10 +7,12 @@ package edu.wpi.first.wpilibj.shuffleboard;
 import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
 
 import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.util.function.FloatSupplier;
 import edu.wpi.first.util.sendable.Sendable;
 import java.util.List;
 import java.util.function.BooleanSupplier;
 import java.util.function.DoubleSupplier;
+import java.util.function.LongSupplier;
 import java.util.function.Supplier;
 
 /** A layout in a Shuffleboard tab. Layouts can contain widgets and other layouts. */
@@ -52,6 +54,11 @@ public class ShuffleboardLayout extends ShuffleboardComponent addString(String title, Supplier valueSupplier) {
     return m_helper.addString(title, valueSupplier);
@@ -62,6 +69,21 @@ public class ShuffleboardLayout extends ShuffleboardComponent addDouble(String title, DoubleSupplier valueSupplier) {
+    return m_helper.addDouble(title, valueSupplier);
+  }
+
+  @Override
+  public SuppliedValueWidget addFloat(String title, FloatSupplier valueSupplier) {
+    return m_helper.addFloat(title, valueSupplier);
+  }
+
+  @Override
+  public SuppliedValueWidget addInteger(String title, LongSupplier valueSupplier) {
+    return m_helper.addInteger(title, valueSupplier);
+  }
+
   @Override
   public SuppliedValueWidget addBoolean(String title, BooleanSupplier valueSupplier) {
     return m_helper.addBoolean(title, valueSupplier);
@@ -79,6 +101,16 @@ public class ShuffleboardLayout extends ShuffleboardComponent addFloatArray(String title, Supplier valueSupplier) {
+    return m_helper.addFloatArray(title, valueSupplier);
+  }
+
+  @Override
+  public SuppliedValueWidget addIntegerArray(String title, Supplier valueSupplier) {
+    return m_helper.addIntegerArray(title, valueSupplier);
+  }
+
   @Override
   public SuppliedValueWidget addBooleanArray(
       String title, Supplier valueSupplier) {
@@ -86,8 +118,9 @@ public class ShuffleboardLayout extends ShuffleboardComponent addRaw(String title, Supplier valueSupplier) {
-    return m_helper.addRaw(title, valueSupplier);
+  public SuppliedValueWidget addRaw(
+      String title, String typeString, Supplier valueSupplier) {
+    return m_helper.addRaw(title, typeString, valueSupplier);
   }
 
   @Override
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java
index 597eb80897..f1feabb9b7 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardTab.java
@@ -5,10 +5,12 @@
 package edu.wpi.first.wpilibj.shuffleboard;
 
 import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.util.function.FloatSupplier;
 import edu.wpi.first.util.sendable.Sendable;
 import java.util.List;
 import java.util.function.BooleanSupplier;
 import java.util.function.DoubleSupplier;
+import java.util.function.LongSupplier;
 import java.util.function.Supplier;
 
 /**
@@ -66,6 +68,11 @@ public final class ShuffleboardTab implements ShuffleboardContainer {
     return m_helper.add(title, defaultValue);
   }
 
+  @Override
+  public SimpleWidget add(String title, String typeString, Object defaultValue) {
+    return m_helper.add(title, typeString, defaultValue);
+  }
+
   @Override
   public SuppliedValueWidget addString(String title, Supplier valueSupplier) {
     return m_helper.addString(title, valueSupplier);
@@ -76,6 +83,21 @@ public final class ShuffleboardTab implements ShuffleboardContainer {
     return m_helper.addNumber(title, valueSupplier);
   }
 
+  @Override
+  public SuppliedValueWidget addDouble(String title, DoubleSupplier valueSupplier) {
+    return m_helper.addDouble(title, valueSupplier);
+  }
+
+  @Override
+  public SuppliedValueWidget addFloat(String title, FloatSupplier valueSupplier) {
+    return m_helper.addFloat(title, valueSupplier);
+  }
+
+  @Override
+  public SuppliedValueWidget addInteger(String title, LongSupplier valueSupplier) {
+    return m_helper.addInteger(title, valueSupplier);
+  }
+
   @Override
   public SuppliedValueWidget addBoolean(String title, BooleanSupplier valueSupplier) {
     return m_helper.addBoolean(title, valueSupplier);
@@ -93,6 +115,16 @@ public final class ShuffleboardTab implements ShuffleboardContainer {
     return m_helper.addDoubleArray(title, valueSupplier);
   }
 
+  @Override
+  public SuppliedValueWidget addFloatArray(String title, Supplier valueSupplier) {
+    return m_helper.addFloatArray(title, valueSupplier);
+  }
+
+  @Override
+  public SuppliedValueWidget addIntegerArray(String title, Supplier valueSupplier) {
+    return m_helper.addIntegerArray(title, valueSupplier);
+  }
+
   @Override
   public SuppliedValueWidget addBooleanArray(
       String title, Supplier valueSupplier) {
@@ -100,8 +132,9 @@ public final class ShuffleboardTab implements ShuffleboardContainer {
   }
 
   @Override
-  public SuppliedValueWidget addRaw(String title, Supplier valueSupplier) {
-    return m_helper.addRaw(title, valueSupplier);
+  public SuppliedValueWidget addRaw(
+      String title, String typeString, Supplier valueSupplier) {
+    return m_helper.addRaw(title, typeString, valueSupplier);
   }
 
   @Override
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java
index 2043432581..33405c70c4 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SimpleWidget.java
@@ -4,12 +4,13 @@
 
 package edu.wpi.first.wpilibj.shuffleboard;
 
+import edu.wpi.first.networktables.GenericEntry;
 import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
 
 /** A Shuffleboard widget that handles a single data point such as a number or string. */
-public final class SimpleWidget extends ShuffleboardWidget {
-  private NetworkTableEntry m_entry;
+public final class SimpleWidget extends ShuffleboardWidget implements AutoCloseable {
+  private String m_typeString = "";
+  private GenericEntry m_entry;
 
   SimpleWidget(ShuffleboardContainer parent, String title) {
     super(parent, title);
@@ -20,18 +21,39 @@ public final class SimpleWidget extends ShuffleboardWidget {
    *
    * @return The NetworkTable entry that contains the data for this widget.
    */
-  public NetworkTableEntry getEntry() {
+  public GenericEntry getEntry() {
     if (m_entry == null) {
       forceGenerate();
     }
     return m_entry;
   }
 
+  /**
+   * Gets the NetworkTable entry that contains the data for this widget.
+   *
+   * @param typeString NetworkTable type string
+   * @return The NetworkTable entry that contains the data for this widget.
+   */
+  public GenericEntry getEntry(String typeString) {
+    if (m_entry == null) {
+      m_typeString = typeString;
+      forceGenerate();
+    }
+    return m_entry;
+  }
+
+  @Override
+  public void close() {
+    if (m_entry != null) {
+      m_entry.close();
+    }
+  }
+
   @Override
   public void buildInto(NetworkTable parentTable, NetworkTable metaTable) {
     buildMetadata(metaTable);
     if (m_entry == null) {
-      m_entry = parentTable.getEntry(getTitle());
+      m_entry = parentTable.getTopic(getTitle()).getGenericEntry(m_typeString);
     }
   }
 
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java
index 497b30f036..3ab8b1e308 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/shuffleboard/SuppliedValueWidget.java
@@ -4,8 +4,10 @@
 
 package edu.wpi.first.wpilibj.shuffleboard;
 
+import edu.wpi.first.networktables.BooleanPublisher;
+import edu.wpi.first.networktables.BooleanTopic;
+import edu.wpi.first.networktables.GenericPublisher;
 import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
 import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
@@ -14,34 +16,52 @@ import java.util.function.Supplier;
  *
  * @param  the type of values in the widget
  */
-public final class SuppliedValueWidget extends ShuffleboardWidget> {
+public final class SuppliedValueWidget extends ShuffleboardWidget>
+    implements AutoCloseable {
+  private final String m_typeString;
   private final Supplier m_supplier;
-  private final BiConsumer m_setter;
+  private final BiConsumer m_setter;
+  private BooleanPublisher m_controllablePub;
+  private GenericPublisher m_entry;
 
   /**
    * Package-private constructor for use by the Shuffleboard API.
    *
    * @param parent the parent container for the widget
    * @param title the title of the widget
+   * @param typeString the NetworkTables string type
    * @param supplier the supplier for values to place in the NetworkTable entry
    * @param setter the function for placing values in the NetworkTable entry
    */
   SuppliedValueWidget(
       ShuffleboardContainer parent,
       String title,
+      String typeString,
       Supplier supplier,
-      BiConsumer setter) {
+      BiConsumer setter) {
     super(parent, title);
-    this.m_supplier = supplier;
-    this.m_setter = setter;
+    m_typeString = typeString;
+    m_supplier = supplier;
+    m_setter = setter;
   }
 
   @Override
   public void buildInto(NetworkTable parentTable, NetworkTable metaTable) {
     buildMetadata(metaTable);
-    metaTable.getEntry("Controllable").setBoolean(false);
+    m_controllablePub = new BooleanTopic(metaTable.getTopic("Controllable")).publish();
+    m_controllablePub.set(false);
 
-    NetworkTableEntry entry = parentTable.getEntry(getTitle());
-    m_setter.accept(entry, m_supplier.get());
+    m_entry = parentTable.getTopic(getTitle()).genericPublish(m_typeString);
+    m_setter.accept(m_entry, m_supplier.get());
+  }
+
+  @Override
+  public void close() {
+    if (m_controllablePub != null) {
+      m_controllablePub.close();
+    }
+    if (m_entry != null) {
+      m_entry.close();
+    }
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java
index 4ae8c57a07..bfacb428cd 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Field2d.java
@@ -29,7 +29,7 @@ import java.util.List;
  * using the getObject() function. Other objects can also have multiple poses (which will show the
  * object at multiple locations).
  */
-public class Field2d implements NTSendable {
+public class Field2d implements NTSendable, AutoCloseable {
   /** Constructor. */
   public Field2d() {
     FieldObject2d obj = new FieldObject2d("Robot");
@@ -38,6 +38,13 @@ public class Field2d implements NTSendable {
     SendableRegistry.add(this, "Field");
   }
 
+  @Override
+  public void close() {
+    for (FieldObject2d obj : m_objects) {
+      obj.close();
+    }
+  }
+
   /**
    * Set the robot pose from a Pose object.
    *
@@ -83,7 +90,7 @@ public class Field2d implements NTSendable {
     m_objects.add(obj);
     if (m_table != null) {
       synchronized (obj) {
-        obj.m_entry = m_table.getEntry(name);
+        obj.m_entry = m_table.getDoubleArrayTopic(name).getEntry(new double[] {});
       }
     }
     return obj;
@@ -106,7 +113,7 @@ public class Field2d implements NTSendable {
       m_table = builder.getTable();
       for (FieldObject2d obj : m_objects) {
         synchronized (obj) {
-          obj.m_entry = m_table.getEntry(obj.m_name);
+          obj.m_entry = m_table.getDoubleArrayTopic(obj.m_name).getEntry(new double[] {});
           obj.updateEntry(true);
         }
       }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java
index 8b24430c7e..61e4f9e613 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/FieldObject2d.java
@@ -8,14 +8,12 @@ import edu.wpi.first.math.geometry.Pose2d;
 import edu.wpi.first.math.geometry.Rotation2d;
 import edu.wpi.first.math.geometry.Translation2d;
 import edu.wpi.first.math.trajectory.Trajectory;
-import edu.wpi.first.networktables.NetworkTableEntry;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
+import edu.wpi.first.networktables.DoubleArrayEntry;
 import java.util.ArrayList;
 import java.util.List;
 
 /** Game field object on a Field2d. */
-public class FieldObject2d {
+public class FieldObject2d implements AutoCloseable {
   /**
    * Package-local constructor.
    *
@@ -25,6 +23,11 @@ public class FieldObject2d {
     m_name = name;
   }
 
+  @Override
+  public void close() {
+    m_entry.close();
+  }
+
   /**
    * Set the pose from a Pose object.
    *
@@ -116,39 +119,20 @@ public class FieldObject2d {
       return;
     }
 
-    if (m_poses.size() < (255 / 3)) {
-      double[] arr = new double[m_poses.size() * 3];
-      int ndx = 0;
-      for (Pose2d pose : m_poses) {
-        Translation2d translation = pose.getTranslation();
-        arr[ndx + 0] = translation.getX();
-        arr[ndx + 1] = translation.getY();
-        arr[ndx + 2] = pose.getRotation().getDegrees();
-        ndx += 3;
-      }
+    double[] arr = new double[m_poses.size() * 3];
+    int ndx = 0;
+    for (Pose2d pose : m_poses) {
+      Translation2d translation = pose.getTranslation();
+      arr[ndx + 0] = translation.getX();
+      arr[ndx + 1] = translation.getY();
+      arr[ndx + 2] = pose.getRotation().getDegrees();
+      ndx += 3;
+    }
 
-      if (setDefault) {
-        m_entry.setDefaultDoubleArray(arr);
-      } else {
-        m_entry.setDoubleArray(arr);
-      }
+    if (setDefault) {
+      m_entry.setDefault(arr);
     } else {
-      // send as raw array of doubles if too big for NT array
-      ByteBuffer output = ByteBuffer.allocate(m_poses.size() * 3 * 8);
-      output.order(ByteOrder.BIG_ENDIAN);
-
-      for (Pose2d pose : m_poses) {
-        Translation2d translation = pose.getTranslation();
-        output.putDouble(translation.getX());
-        output.putDouble(translation.getY());
-        output.putDouble(pose.getRotation().getDegrees());
-      }
-
-      if (setDefault) {
-        m_entry.setDefaultRaw(output.array());
-      } else {
-        m_entry.forceSetRaw(output.array());
-      }
+      m_entry.set(arr);
     }
   }
 
@@ -157,7 +141,7 @@ public class FieldObject2d {
       return;
     }
 
-    double[] arr = m_entry.getDoubleArray((double[]) null);
+    double[] arr = m_entry.get((double[]) null);
     if (arr != null) {
       if ((arr.length % 3) != 0) {
         return;
@@ -167,31 +151,10 @@ public class FieldObject2d {
       for (int i = 0; i < arr.length; i += 3) {
         m_poses.add(new Pose2d(arr[i], arr[i + 1], Rotation2d.fromDegrees(arr[i + 2])));
       }
-    } else {
-      // read as raw array of doubles
-      byte[] data = m_entry.getRaw((byte[]) null);
-      if (data == null) {
-        return;
-      }
-
-      // must be triples of doubles
-      if ((data.length % (3 * 8)) != 0) {
-        return;
-      }
-      ByteBuffer input = ByteBuffer.wrap(data);
-      input.order(ByteOrder.BIG_ENDIAN);
-
-      m_poses.clear();
-      for (int i = 0; i < (data.length / (3 * 8)); i++) {
-        double x = input.getDouble();
-        double y = input.getDouble();
-        double rot = input.getDouble();
-        m_poses.add(new Pose2d(x, y, Rotation2d.fromDegrees(rot)));
-      }
     }
   }
 
   String m_name;
-  NetworkTableEntry m_entry;
+  DoubleArrayEntry m_entry;
   private final List m_poses = new ArrayList<>();
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Mechanism2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Mechanism2d.java
index 480048958d..d368d35b1e 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Mechanism2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/Mechanism2d.java
@@ -4,9 +4,11 @@
 
 package edu.wpi.first.wpilibj.smartdashboard;
 
+import edu.wpi.first.networktables.DoubleArrayPublisher;
 import edu.wpi.first.networktables.NTSendable;
 import edu.wpi.first.networktables.NTSendableBuilder;
 import edu.wpi.first.networktables.NetworkTable;
+import edu.wpi.first.networktables.StringPublisher;
 import edu.wpi.first.wpilibj.util.Color8Bit;
 import java.util.HashMap;
 import java.util.Map;
@@ -23,12 +25,13 @@ import java.util.Map.Entry;
  * @see MechanismLigament2d
  * @see MechanismRoot2d
  */
-public final class Mechanism2d implements NTSendable {
-  private static final String kBackgroundColor = "backgroundColor";
+public final class Mechanism2d implements NTSendable, AutoCloseable {
   private NetworkTable m_table;
   private final Map m_roots;
   private final double[] m_dims = new double[2];
   private String m_color;
+  private DoubleArrayPublisher m_dimsPub;
+  private StringPublisher m_colorPub;
 
   /**
    * Create a new Mechanism2d with the given dimensions and default color (dark blue).
@@ -58,6 +61,19 @@ public final class Mechanism2d implements NTSendable {
     setBackgroundColor(backgroundColor);
   }
 
+  @Override
+  public void close() {
+    if (m_dimsPub != null) {
+      m_dimsPub.close();
+    }
+    if (m_colorPub != null) {
+      m_colorPub.close();
+    }
+    for (MechanismRoot2d root : m_roots.values()) {
+      root.close();
+    }
+  }
+
   /**
    * Get or create a root in this Mechanism2d with the given name and position.
    *
@@ -88,9 +104,9 @@ public final class Mechanism2d implements NTSendable {
    * @param color the new color
    */
   public synchronized void setBackgroundColor(Color8Bit color) {
-    this.m_color = color.toHexString();
-    if (m_table != null) {
-      m_table.getEntry(kBackgroundColor).setString(m_color);
+    m_color = color.toHexString();
+    if (m_colorPub != null) {
+      m_colorPub.set(m_color);
     }
   }
 
@@ -99,8 +115,16 @@ public final class Mechanism2d implements NTSendable {
     builder.setSmartDashboardType("Mechanism2d");
     synchronized (this) {
       m_table = builder.getTable();
-      m_table.getEntry("dims").setDoubleArray(m_dims);
-      m_table.getEntry(kBackgroundColor).setString(m_color);
+      if (m_dimsPub != null) {
+        m_dimsPub.close();
+      }
+      m_dimsPub = m_table.getDoubleArrayTopic("dims").publish();
+      m_dimsPub.set(m_dims);
+      if (m_colorPub != null) {
+        m_colorPub.close();
+      }
+      m_colorPub = m_table.getStringTopic("backgroundColor").publish();
+      m_colorPub.set(m_color);
       for (Entry entry : m_roots.entrySet()) {
         String name = entry.getKey();
         MechanismRoot2d root = entry.getValue();
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java
index 8f0386f78c..28484a39e4 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismLigament2d.java
@@ -5,8 +5,10 @@
 package edu.wpi.first.wpilibj.smartdashboard;
 
 import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.networktables.DoubleEntry;
 import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
+import edu.wpi.first.networktables.StringEntry;
+import edu.wpi.first.networktables.StringPublisher;
 import edu.wpi.first.wpilibj.util.Color8Bit;
 
 /**
@@ -16,14 +18,15 @@ import edu.wpi.first.wpilibj.util.Color8Bit;
  * @see Mechanism2d
  */
 public class MechanismLigament2d extends MechanismObject2d {
+  private StringPublisher m_typePub;
   private double m_angle;
-  private NetworkTableEntry m_angleEntry;
+  private DoubleEntry m_angleEntry;
   private String m_color;
-  private NetworkTableEntry m_colorEntry;
+  private StringEntry m_colorEntry;
   private double m_length;
-  private NetworkTableEntry m_lengthEntry;
+  private DoubleEntry m_lengthEntry;
   private double m_weight;
-  private NetworkTableEntry m_weightEntry;
+  private DoubleEntry m_weightEntry;
 
   /**
    * Create a new ligament.
@@ -54,6 +57,26 @@ public class MechanismLigament2d extends MechanismObject2d {
     this(name, length, angle, 10, new Color8Bit(235, 137, 52));
   }
 
+  @Override
+  public void close() {
+    super.close();
+    if (m_typePub != null) {
+      m_typePub.close();
+    }
+    if (m_angleEntry != null) {
+      m_angleEntry.close();
+    }
+    if (m_colorEntry != null) {
+      m_colorEntry.close();
+    }
+    if (m_lengthEntry != null) {
+      m_lengthEntry.close();
+    }
+    if (m_weightEntry != null) {
+      m_weightEntry.close();
+    }
+  }
+
   /**
    * Set the ligament's angle relative to its parent.
    *
@@ -61,7 +84,9 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized void setAngle(double degrees) {
     m_angle = degrees;
-    flush();
+    if (m_angleEntry != null) {
+      m_angleEntry.set(degrees);
+    }
   }
 
   /**
@@ -80,7 +105,7 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized double getAngle() {
     if (m_angleEntry != null) {
-      m_angle = m_angleEntry.getDouble(0.0);
+      m_angle = m_angleEntry.get();
     }
     return m_angle;
   }
@@ -92,7 +117,9 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized void setLength(double length) {
     m_length = length;
-    flush();
+    if (m_lengthEntry != null) {
+      m_lengthEntry.set(length);
+    }
   }
 
   /**
@@ -102,7 +129,7 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized double getLength() {
     if (m_lengthEntry != null) {
-      m_length = m_lengthEntry.getDouble(0.0);
+      m_length = m_lengthEntry.get();
     }
     return m_length;
   }
@@ -114,7 +141,9 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized void setColor(Color8Bit color) {
     m_color = String.format("#%02X%02X%02X", color.red, color.green, color.blue);
-    flush();
+    if (m_colorEntry != null) {
+      m_colorEntry.set(m_color);
+    }
   }
 
   /**
@@ -124,7 +153,7 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized Color8Bit getColor() {
     if (m_colorEntry != null) {
-      m_color = m_colorEntry.getString("");
+      m_color = m_colorEntry.get();
     }
     int r = 0;
     int g = 0;
@@ -150,7 +179,9 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized void setLineWeight(double weight) {
     m_weight = weight;
-    flush();
+    if (m_weightEntry != null) {
+      m_weightEntry.set(weight);
+    }
   }
 
   /**
@@ -160,34 +191,41 @@ public class MechanismLigament2d extends MechanismObject2d {
    */
   public synchronized double getLineWeight() {
     if (m_weightEntry != null) {
-      m_weight = m_weightEntry.getDouble(0.0);
+      m_weight = m_weightEntry.get();
     }
     return m_weight;
   }
 
   @Override
   protected void updateEntries(NetworkTable table) {
-    table.getEntry(".type").setString("line");
-    m_angleEntry = table.getEntry("angle");
-    m_lengthEntry = table.getEntry("length");
-    m_colorEntry = table.getEntry("color");
-    m_weightEntry = table.getEntry("weight");
-    flush();
-  }
+    if (m_typePub != null) {
+      m_typePub.close();
+    }
+    m_typePub = table.getStringTopic(".type").publish();
+    m_typePub.set("line");
 
-  /** Flush latest data to NT. */
-  private void flush() {
     if (m_angleEntry != null) {
-      m_angleEntry.setDouble(m_angle);
+      m_angleEntry.close();
     }
+    m_angleEntry = table.getDoubleTopic("angle").getEntry(0.0);
+    m_angleEntry.set(m_angle);
+
     if (m_lengthEntry != null) {
-      m_lengthEntry.setDouble(m_length);
+      m_lengthEntry.close();
     }
+    m_lengthEntry = table.getDoubleTopic("length").getEntry(0.0);
+    m_lengthEntry.set(m_length);
+
     if (m_colorEntry != null) {
-      m_colorEntry.setString(m_color);
+      m_colorEntry.close();
     }
+    m_colorEntry = table.getStringTopic("color").getEntry("");
+    m_colorEntry.set(m_color);
+
     if (m_weightEntry != null) {
-      m_weightEntry.setDouble(m_weight);
+      m_weightEntry.close();
     }
+    m_weightEntry = table.getDoubleTopic("weight").getEntry(0.0);
+    m_weightEntry.set(m_weight);
   }
 }
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java
index 584660aa85..2d13ce0a55 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismObject2d.java
@@ -16,7 +16,7 @@ import java.util.Map;
  *
  * @see Mechanism2d
  */
-public abstract class MechanismObject2d {
+public abstract class MechanismObject2d implements AutoCloseable {
   /** Relative to parent. */
   private final String m_name;
 
@@ -32,6 +32,13 @@ public abstract class MechanismObject2d {
     m_name = name;
   }
 
+  @Override
+  public void close() {
+    for (MechanismObject2d obj : m_objects.values()) {
+      obj.close();
+    }
+  }
+
   /**
    * Append a Mechanism object that is based on this one.
    *
diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismRoot2d.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismRoot2d.java
index ee804b7df3..6091132755 100644
--- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismRoot2d.java
+++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/MechanismRoot2d.java
@@ -4,8 +4,8 @@
 
 package edu.wpi.first.wpilibj.smartdashboard;
 
+import edu.wpi.first.networktables.DoublePublisher;
 import edu.wpi.first.networktables.NetworkTable;
-import edu.wpi.first.networktables.NetworkTableEntry;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -19,14 +19,14 @@ import java.util.Map;
  *
  * 

Append other nodes by using {@link #append(MechanismObject2d)}. */ -public final class MechanismRoot2d { +public final class MechanismRoot2d implements AutoCloseable { private final String m_name; private NetworkTable m_table; private final Map m_objects = new HashMap<>(1); private double m_x; - private NetworkTableEntry m_xEntry; + private DoublePublisher m_xPub; private double m_y; - private NetworkTableEntry m_yEntry; + private DoublePublisher m_yPub; /** * Package-private constructor for roots. @@ -41,6 +41,19 @@ public final class MechanismRoot2d { m_y = y; } + @Override + public void close() { + if (m_xPub != null) { + m_xPub.close(); + } + if (m_yPub != null) { + m_yPub.close(); + } + for (MechanismObject2d obj : m_objects.values()) { + obj.close(); + } + } + /** * Append a Mechanism object that is based on this one. * @@ -75,8 +88,14 @@ public final class MechanismRoot2d { synchronized void update(NetworkTable table) { m_table = table; - m_xEntry = m_table.getEntry("x"); - m_yEntry = m_table.getEntry("y"); + if (m_xPub != null) { + m_xPub.close(); + } + m_xPub = m_table.getDoubleTopic("x").publish(); + if (m_yPub != null) { + m_yPub.close(); + } + m_yPub = m_table.getDoubleTopic("y").publish(); flush(); for (MechanismObject2d obj : m_objects.values()) { obj.update(m_table.getSubTable(obj.getName())); @@ -88,11 +107,11 @@ public final class MechanismRoot2d { } private void flush() { - if (m_xEntry != null) { - m_xEntry.setDouble(m_x); + if (m_xPub != null) { + m_xPub.set(m_x); } - if (m_yEntry != null) { - m_yEntry.setDouble(m_y); + if (m_yPub != null) { + m_yPub.set(m_y); } } } diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java index 38d8ffb308..3c1e17c917 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableBuilderImpl.java @@ -4,58 +4,132 @@ package edu.wpi.first.wpilibj.smartdashboard; -import edu.wpi.first.networktables.EntryListenerFlags; +import edu.wpi.first.networktables.BooleanArrayPublisher; +import edu.wpi.first.networktables.BooleanArraySubscriber; +import edu.wpi.first.networktables.BooleanArrayTopic; +import edu.wpi.first.networktables.BooleanPublisher; +import edu.wpi.first.networktables.BooleanSubscriber; +import edu.wpi.first.networktables.BooleanTopic; +import edu.wpi.first.networktables.DoubleArrayPublisher; +import edu.wpi.first.networktables.DoubleArraySubscriber; +import edu.wpi.first.networktables.DoubleArrayTopic; +import edu.wpi.first.networktables.DoublePublisher; +import edu.wpi.first.networktables.DoubleSubscriber; +import edu.wpi.first.networktables.DoubleTopic; +import edu.wpi.first.networktables.FloatArrayPublisher; +import edu.wpi.first.networktables.FloatArraySubscriber; +import edu.wpi.first.networktables.FloatArrayTopic; +import edu.wpi.first.networktables.FloatPublisher; +import edu.wpi.first.networktables.FloatSubscriber; +import edu.wpi.first.networktables.FloatTopic; +import edu.wpi.first.networktables.IntegerArrayPublisher; +import edu.wpi.first.networktables.IntegerArraySubscriber; +import edu.wpi.first.networktables.IntegerArrayTopic; +import edu.wpi.first.networktables.IntegerPublisher; +import edu.wpi.first.networktables.IntegerSubscriber; +import edu.wpi.first.networktables.IntegerTopic; import edu.wpi.first.networktables.NTSendableBuilder; import edu.wpi.first.networktables.NetworkTable; -import edu.wpi.first.networktables.NetworkTableEntry; -import edu.wpi.first.networktables.NetworkTableValue; +import edu.wpi.first.networktables.Publisher; +import edu.wpi.first.networktables.RawPublisher; +import edu.wpi.first.networktables.RawSubscriber; +import edu.wpi.first.networktables.RawTopic; +import edu.wpi.first.networktables.StringArrayPublisher; +import edu.wpi.first.networktables.StringArraySubscriber; +import edu.wpi.first.networktables.StringArrayTopic; +import edu.wpi.first.networktables.StringPublisher; +import edu.wpi.first.networktables.StringSubscriber; +import edu.wpi.first.networktables.StringTopic; +import edu.wpi.first.networktables.Subscriber; +import edu.wpi.first.networktables.Topic; +import edu.wpi.first.util.WPIUtilJNI; import edu.wpi.first.util.function.BooleanConsumer; +import edu.wpi.first.util.function.FloatConsumer; +import edu.wpi.first.util.function.FloatSupplier; import java.util.ArrayList; import java.util.List; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.DoubleConsumer; import java.util.function.DoubleSupplier; -import java.util.function.Function; +import java.util.function.LongConsumer; +import java.util.function.LongSupplier; import java.util.function.Supplier; public class SendableBuilderImpl implements NTSendableBuilder { - private static class Property implements AutoCloseable { - Property(NetworkTable table, String key) { - m_entry = table.getEntry(key); - } - - @Override - public void close() { - stopListener(); - } - - void startListener() { - if (m_entry.isValid() && m_listener == 0 && m_createListener != null) { - m_listener = m_createListener.apply(m_entry); - } - } - - void stopListener() { - if (m_entry.isValid() && m_listener != 0) { - m_entry.removeListener(m_listener); - m_listener = 0; - } - } - - final NetworkTableEntry m_entry; - int m_listener; - Consumer m_update; - Function m_createListener; + @FunctionalInterface + private interface TimedConsumer { + void accept(T value, long time); } - private final List m_properties = new ArrayList<>(); + private static class Property

+ implements AutoCloseable { + @Override + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public void close() { + try { + if (m_pub != null) { + m_pub.close(); + } + if (m_sub != null) { + m_sub.close(); + } + } catch (Exception e) { + // ignore + } + } + + void update(boolean controllable, long time) { + if (controllable && m_sub != null && m_updateLocal != null) { + m_updateLocal.accept(m_sub); + } else if (m_pub != null && m_updateNetwork != null) { + m_updateNetwork.accept(m_pub, time); + } + } + + P m_pub; + S m_sub; + TimedConsumer

m_updateNetwork; + Consumer m_updateLocal; + } + + private final List> m_properties = new ArrayList<>(); private Runnable m_safeState; private final List m_updateTables = new ArrayList<>(); private NetworkTable m_table; - private NetworkTableEntry m_controllableEntry; + private boolean m_controllable; private boolean m_actuator; + private BooleanPublisher m_controllablePub; + private StringPublisher m_typePub; + private BooleanPublisher m_actuatorPub; + + private final List m_closeables = new ArrayList<>(); + + @Override + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public void close() { + if (m_controllablePub != null) { + m_controllablePub.close(); + } + if (m_typePub != null) { + m_typePub.close(); + } + if (m_actuatorPub != null) { + m_actuatorPub.close(); + } + for (Property property : m_properties) { + property.close(); + } + for (AutoCloseable closeable : m_closeables) { + try { + closeable.close(); + } catch (Exception e) { + // ignore + } + } + } + /** * Set the network table. Must be called prior to any Add* functions being called. * @@ -63,7 +137,8 @@ public class SendableBuilderImpl implements NTSendableBuilder { */ public void setTable(NetworkTable table) { m_table = table; - m_controllableEntry = table.getEntry(".controllable"); + m_controllablePub = table.getBooleanTopic(".controllable").publish(); + m_controllablePub.setDefault(false); } /** @@ -98,10 +173,9 @@ public class SendableBuilderImpl implements NTSendableBuilder { /** Update the network table values by calling the getters for all properties. */ @Override public void update() { - for (Property property : m_properties) { - if (property.m_update != null) { - property.m_update.accept(property.m_entry); - } + long time = WPIUtilJNI.now(); + for (Property property : m_properties) { + property.update(m_controllable, time); } for (Runnable updateTable : m_updateTables) { updateTable.run(); @@ -110,21 +184,17 @@ public class SendableBuilderImpl implements NTSendableBuilder { /** Hook setters for all properties. */ public void startListeners() { - for (Property property : m_properties) { - property.startListener(); - } - if (m_controllableEntry != null) { - m_controllableEntry.setBoolean(true); + m_controllable = true; + if (m_controllablePub != null) { + m_controllablePub.set(true); } } /** Unhook setters for all properties. */ public void stopListeners() { - for (Property property : m_properties) { - property.stopListener(); - } - if (m_controllableEntry != null) { - m_controllableEntry.setBoolean(false); + m_controllable = false; + if (m_controllablePub != null) { + m_controllablePub.set(false); } } @@ -154,9 +224,17 @@ public class SendableBuilderImpl implements NTSendableBuilder { @Override public void clearProperties() { stopListeners(); + for (Property property : m_properties) { + property.close(); + } m_properties.clear(); } + @Override + public void addCloseable(AutoCloseable closeable) { + m_closeables.add(closeable); + } + /** * Set the string representation of the named data type that will be used by the smart dashboard * for this sendable. @@ -165,7 +243,10 @@ public class SendableBuilderImpl implements NTSendableBuilder { */ @Override public void setSmartDashboardType(String type) { - m_table.getEntry(".type").setString(type); + if (m_typePub == null) { + m_typePub = m_table.getStringTopic(".type").publish(); + } + m_typePub.set(type); } /** @@ -176,7 +257,10 @@ public class SendableBuilderImpl implements NTSendableBuilder { */ @Override public void setActuator(boolean value) { - m_table.getEntry(".actuator").setBoolean(value); + if (m_actuatorPub == null) { + m_actuatorPub = m_table.getBooleanTopic(".actuator").publish(); + } + m_actuatorPub.set(value); m_actuator = value; } @@ -194,7 +278,7 @@ public class SendableBuilderImpl implements NTSendableBuilder { /** * Set the function that should be called to update the network table for things other than * properties. Note this function is not passed the network table object; instead it should use - * the entry handles returned by getEntry(). + * the topics returned by getTopic(). * * @param func function */ @@ -211,8 +295,8 @@ public class SendableBuilderImpl implements NTSendableBuilder { * @return Network table entry */ @Override - public NetworkTableEntry getEntry(String key) { - return m_table.getEntry(key); + public Topic getTopic(String key) { + return m_table.getTopic(key); } /** @@ -224,23 +308,74 @@ public class SendableBuilderImpl implements NTSendableBuilder { */ @Override public void addBooleanProperty(String key, BooleanSupplier getter, BooleanConsumer setter) { - Property property = new Property(m_table, key); + Property property = new Property<>(); + BooleanTopic topic = m_table.getBooleanTopic(key); if (getter != null) { - property.m_update = entry -> entry.setBoolean(getter.getAsBoolean()); + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.getAsBoolean(), time); } if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - if (event.value.isBoolean()) { - SmartDashboard.postListenerTask( - () -> setter.accept(event.value.getBoolean())); - } - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); + property.m_sub = topic.subscribe(false); + property.m_updateLocal = + sub -> { + for (boolean val : sub.readQueueValues()) { + setter.accept(val); + } + }; + } + m_properties.add(property); + } + + /** + * Add an integer property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + @Override + public void addIntegerProperty(String key, LongSupplier getter, LongConsumer setter) { + Property property = new Property<>(); + IntegerTopic topic = m_table.getIntegerTopic(key); + if (getter != null) { + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.getAsLong(), time); + } + if (setter != null) { + property.m_sub = topic.subscribe(0); + property.m_updateLocal = + sub -> { + for (long val : sub.readQueueValues()) { + setter.accept(val); + } + }; + } + m_properties.add(property); + } + + /** + * Add a float property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + @Override + public void addFloatProperty(String key, FloatSupplier getter, FloatConsumer setter) { + Property property = new Property<>(); + FloatTopic topic = m_table.getFloatTopic(key); + if (getter != null) { + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.getAsFloat(), time); + } + if (setter != null) { + property.m_sub = topic.subscribe(0.0f); + property.m_updateLocal = + sub -> { + for (float val : sub.readQueueValues()) { + setter.accept(val); + } + }; } m_properties.add(property); } @@ -254,22 +389,20 @@ public class SendableBuilderImpl implements NTSendableBuilder { */ @Override public void addDoubleProperty(String key, DoubleSupplier getter, DoubleConsumer setter) { - Property property = new Property(m_table, key); + Property property = new Property<>(); + DoubleTopic topic = m_table.getDoubleTopic(key); if (getter != null) { - property.m_update = entry -> entry.setDouble(getter.getAsDouble()); + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.getAsDouble(), time); } if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - if (event.value.isDouble()) { - SmartDashboard.postListenerTask(() -> setter.accept(event.value.getDouble())); - } - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); + property.m_sub = topic.subscribe(0.0); + property.m_updateLocal = + sub -> { + for (double val : sub.readQueueValues()) { + setter.accept(val); + } + }; } m_properties.add(property); } @@ -283,22 +416,20 @@ public class SendableBuilderImpl implements NTSendableBuilder { */ @Override public void addStringProperty(String key, Supplier getter, Consumer setter) { - Property property = new Property(m_table, key); + Property property = new Property<>(); + StringTopic topic = m_table.getStringTopic(key); if (getter != null) { - property.m_update = entry -> entry.setString(getter.get()); + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.get(), time); } if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - if (event.value.isString()) { - SmartDashboard.postListenerTask(() -> setter.accept(event.value.getString())); - } - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); + property.m_sub = topic.subscribe(""); + property.m_updateLocal = + sub -> { + for (String val : sub.readQueueValues()) { + setter.accept(val); + } + }; } m_properties.add(property); } @@ -313,23 +444,76 @@ public class SendableBuilderImpl implements NTSendableBuilder { @Override public void addBooleanArrayProperty( String key, Supplier getter, Consumer setter) { - Property property = new Property(m_table, key); + Property property = new Property<>(); + BooleanArrayTopic topic = m_table.getBooleanArrayTopic(key); if (getter != null) { - property.m_update = entry -> entry.setBooleanArray(getter.get()); + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.get(), time); } if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - if (event.value.isBooleanArray()) { - SmartDashboard.postListenerTask( - () -> setter.accept(event.value.getBooleanArray())); - } - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); + property.m_sub = topic.subscribe(new boolean[] {}); + property.m_updateLocal = + sub -> { + for (boolean[] val : sub.readQueueValues()) { + setter.accept(val); + } + }; + } + m_properties.add(property); + } + + /** + * Add an integer array property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + @Override + public void addIntegerArrayProperty( + String key, Supplier getter, Consumer setter) { + Property property = new Property<>(); + IntegerArrayTopic topic = m_table.getIntegerArrayTopic(key); + if (getter != null) { + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.get(), time); + } + if (setter != null) { + property.m_sub = topic.subscribe(new long[] {}); + property.m_updateLocal = + sub -> { + for (long[] val : sub.readQueueValues()) { + setter.accept(val); + } + }; + } + m_properties.add(property); + } + + /** + * Add a float array property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + @Override + public void addFloatArrayProperty( + String key, Supplier getter, Consumer setter) { + Property property = new Property<>(); + FloatArrayTopic topic = m_table.getFloatArrayTopic(key); + if (getter != null) { + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.get(), time); + } + if (setter != null) { + property.m_sub = topic.subscribe(new float[] {}); + property.m_updateLocal = + sub -> { + for (float[] val : sub.readQueueValues()) { + setter.accept(val); + } + }; } m_properties.add(property); } @@ -344,23 +528,20 @@ public class SendableBuilderImpl implements NTSendableBuilder { @Override public void addDoubleArrayProperty( String key, Supplier getter, Consumer setter) { - Property property = new Property(m_table, key); + Property property = new Property<>(); + DoubleArrayTopic topic = m_table.getDoubleArrayTopic(key); if (getter != null) { - property.m_update = entry -> entry.setDoubleArray(getter.get()); + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.get(), time); } if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - if (event.value.isDoubleArray()) { - SmartDashboard.postListenerTask( - () -> setter.accept(event.value.getDoubleArray())); - } - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); + property.m_sub = topic.subscribe(new double[] {}); + property.m_updateLocal = + sub -> { + for (double[] val : sub.readQueueValues()) { + setter.accept(val); + } + }; } m_properties.add(property); } @@ -375,23 +556,20 @@ public class SendableBuilderImpl implements NTSendableBuilder { @Override public void addStringArrayProperty( String key, Supplier getter, Consumer setter) { - Property property = new Property(m_table, key); + Property property = new Property<>(); + StringArrayTopic topic = m_table.getStringArrayTopic(key); if (getter != null) { - property.m_update = entry -> entry.setStringArray(getter.get()); + property.m_pub = topic.publish(); + property.m_updateNetwork = (pub, time) -> pub.set(getter.get(), time); } if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - if (event.value.isStringArray()) { - SmartDashboard.postListenerTask( - () -> setter.accept(event.value.getStringArray())); - } - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); + property.m_sub = topic.subscribe(new String[] {}); + property.m_updateLocal = + sub -> { + for (String[] val : sub.readQueueValues()) { + setter.accept(val); + } + }; } m_properties.add(property); } @@ -400,55 +578,27 @@ public class SendableBuilderImpl implements NTSendableBuilder { * Add a raw property. * * @param key property name + * @param typeString type string * @param getter getter function (returns current value) * @param setter setter function (sets new value) */ @Override - public void addRawProperty(String key, Supplier getter, Consumer setter) { - Property property = new Property(m_table, key); + public void addRawProperty( + String key, String typeString, Supplier getter, Consumer setter) { + Property property = new Property<>(); + RawTopic topic = m_table.getRawTopic(key); if (getter != null) { - property.m_update = entry -> entry.setRaw(getter.get()); + property.m_pub = topic.publish(typeString); + property.m_updateNetwork = (pub, time) -> pub.set(getter.get(), time); } if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - if (event.value.isRaw()) { - SmartDashboard.postListenerTask(() -> setter.accept(event.value.getRaw())); - } - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); - } - m_properties.add(property); - } - - /** - * Add a NetworkTableValue property. - * - * @param key property name - * @param getter getter function (returns current value) - * @param setter setter function (sets new value) - */ - @Override - public void addValueProperty( - String key, Supplier getter, Consumer setter) { - Property property = new Property(m_table, key); - if (getter != null) { - property.m_update = entry -> entry.setValue(getter.get()); - } - if (setter != null) { - property.m_createListener = - entry -> - entry.addListener( - event -> { - SmartDashboard.postListenerTask(() -> setter.accept(event.value)); - }, - EntryListenerFlags.kImmediate - | EntryListenerFlags.kNew - | EntryListenerFlags.kUpdate); + property.m_sub = topic.subscribe(typeString, new byte[] {}); + property.m_updateLocal = + sub -> { + for (byte[] val : sub.readQueueValues()) { + setter.accept(val); + } + }; } m_properties.add(property); } diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java index b18708e530..322938b853 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SendableChooser.java @@ -6,9 +6,12 @@ package edu.wpi.first.wpilibj.smartdashboard; import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam; +import edu.wpi.first.networktables.IntegerPublisher; +import edu.wpi.first.networktables.IntegerTopic; import edu.wpi.first.networktables.NTSendable; import edu.wpi.first.networktables.NTSendableBuilder; -import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.StringPublisher; +import edu.wpi.first.networktables.StringTopic; import edu.wpi.first.util.sendable.SendableRegistry; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -56,6 +59,14 @@ public class SendableChooser implements NTSendable, AutoCloseable { @Override public void close() { SendableRegistry.remove(this); + m_mutex.lock(); + try { + for (StringPublisher pub : m_activePubs) { + pub.close(); + } + } finally { + m_mutex.unlock(); + } } /** @@ -104,13 +115,15 @@ public class SendableChooser implements NTSendable, AutoCloseable { } private String m_selected; - private final List m_activeEntries = new ArrayList<>(); + private final List m_activePubs = new ArrayList<>(); private final ReentrantLock m_mutex = new ReentrantLock(); @Override public void initSendable(NTSendableBuilder builder) { builder.setSmartDashboardType("String Chooser"); - builder.getEntry(INSTANCE).setDouble(m_instance); + IntegerPublisher instancePub = new IntegerTopic(builder.getTopic(INSTANCE)).publish(); + instancePub.set(m_instance); + builder.addCloseable(instancePub); builder.addStringProperty(DEFAULT, () -> m_defaultChoice, null); builder.addStringArrayProperty(OPTIONS, () -> m_map.keySet().toArray(new String[0]), null); builder.addStringProperty( @@ -130,7 +143,7 @@ public class SendableChooser implements NTSendable, AutoCloseable { null); m_mutex.lock(); try { - m_activeEntries.add(builder.getEntry(ACTIVE)); + m_activePubs.add(new StringTopic(builder.getTopic(ACTIVE)).publish()); } finally { m_mutex.unlock(); } @@ -141,8 +154,8 @@ public class SendableChooser implements NTSendable, AutoCloseable { m_mutex.lock(); try { m_selected = val; - for (NetworkTableEntry entry : m_activeEntries) { - entry.setString(val); + for (StringPublisher pub : m_activePubs) { + pub.set(val); } } finally { m_mutex.unlock(); diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java index be5684b4e5..ab635b1dea 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboard.java @@ -11,7 +11,6 @@ import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.util.sendable.Sendable; import edu.wpi.first.util.sendable.SendableRegistry; -import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -25,8 +24,7 @@ import java.util.Set; */ public final class SmartDashboard { /** The {@link NetworkTable} used by {@link SmartDashboard}. */ - private static final NetworkTable table = - NetworkTableInstance.getDefault().getTable("SmartDashboard"); + private static NetworkTable table; /** * A table linking tables in the SmartDashboard to the {@link Sendable} objects they came from. @@ -38,6 +36,7 @@ public final class SmartDashboard { private static final ListenerExecutor listenerExecutor = new ListenerExecutor(); static { + setNetworkTableInstance(NetworkTableInstance.getDefault()); HAL.report(tResourceType.kResourceType_SmartDashboard, 0); } @@ -45,6 +44,16 @@ public final class SmartDashboard { throw new UnsupportedOperationException("This is a utility class!"); } + /** + * Set the NetworkTable instance used for entries. For testing purposes; use with caution. + * + * @param inst NetworkTable instance + */ + public static synchronized void setNetworkTableInstance(NetworkTableInstance inst) { + SmartDashboard.table = inst.getTable("SmartDashboard"); + tablesToData.clear(); + } + /** * Maps the specified key to the specified value in this table. The key can not be null. The value * can be retrieved by calling the get method with a key that is equal to the original key. @@ -165,45 +174,6 @@ public final class SmartDashboard { return getEntry(key).isPersistent(); } - /** - * 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) - */ - public static void setFlags(String key, int flags) { - getEntry(key).setFlags(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) - */ - public static void clearFlags(String key, int flags) { - getEntry(key).clearFlags(flags); - } - - /** - * Returns the flags for the specified key. - * - * @param key the key name - * @return the flags, or 0 if the key is not defined - */ - public static int getFlags(String key) { - return getEntry(key).getFlags(); - } - - /** - * Deletes the specified key in this table. The key can not be null. - * - * @param key the key name - */ - public static void delete(String key) { - table.delete(key); - } - /** * Put a boolean in the table. * @@ -495,18 +465,6 @@ public final class SmartDashboard { return getEntry(key).setRaw(value); } - /** - * Put a raw value (bytes from a byte buffer) in the table. - * - * @param key the key to be assigned to - * @param value the value that will be assigned - * @param len the length of the value - * @return False if the table key already exists with a different type - */ - public static boolean putRaw(String key, ByteBuffer value, int len) { - return getEntry(key).setRaw(value, len); - } - /** * Gets the current value in the table, setting it if it does not exist. * diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java index 70025d2df0..c53da48720 100644 --- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java @@ -9,69 +9,83 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.Topic; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Set; +import java.util.List; import java.util.stream.Stream; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +@Execution(SAME_THREAD) class PreferencesTest { - private final NetworkTable m_table = NetworkTableInstance.getDefault().getTable("Preferences"); + private NetworkTableInstance m_inst; + private NetworkTable m_table; - private static final String kFilename = "networktables.ini"; - - @BeforeAll - static void setupAll() { - NetworkTableInstance.getDefault().stopServer(); - } + private static final String kFilename = "networktables.json"; @BeforeEach void setup(@TempDir Path tempDir) { - m_table.getKeys().forEach(m_table::delete); + m_inst = NetworkTableInstance.create(); + m_table = m_inst.getTable("Preferences"); + Preferences.setNetworkTableInstance(m_inst); Path filepath = tempDir.resolve(kFilename); - try (InputStream is = getClass().getResource("PreferencesTestDefault.ini").openStream()) { + try (InputStream is = getClass().getResource("PreferencesTestDefault.json").openStream()) { Files.copy(is, filepath); } catch (IOException ex) { fail(ex); } - NetworkTableInstance.getDefault().startServer(filepath.toString()); + m_inst.startServer(filepath.toString(), "", 0, 0); + try { + int count = 0; + while ((m_inst.getNetworkMode() & NetworkTableInstance.kNetModeStarting) != 0) { + Thread.sleep(100); + count++; + if (count > 30) { + throw new InterruptedException(); + } + } + } catch (InterruptedException ex) { + fail("interrupted while waiting for server to start"); + } } @AfterEach void cleanup() { - NetworkTableInstance.getDefault().stopServer(); - } - - @AfterAll - static void cleanupAll() { - NetworkTableInstance.getDefault().startServer(); + m_inst.close(); } @Test void removeAllTest() { Preferences.removeAll(); - Set keys = m_table.getKeys(); - keys.remove(".type"); + List keys = new ArrayList<>(); + boolean anyPersistent = false; + for (Topic topic : m_table.getTopics()) { + if (topic.isPersistent()) { + anyPersistent = true; + keys.add(topic.getName()); + } + } - assertTrue( - keys.isEmpty(), + assertFalse( + anyPersistent, "Preferences was not empty! Preferences in table: " + Arrays.toString(keys.toArray())); } @@ -98,19 +112,6 @@ class PreferencesTest { () -> assertFalse(Preferences.getBoolean("checkedValueBoolean", true))); } - @Test - void backupTest() { - Preferences.removeAll(); - - assertAll( - () -> assertEquals(0, Preferences.getLong("checkedValueLong", 0)), - () -> assertEquals(0, Preferences.getDouble("checkedValueDouble", 0), 1e-6), - () -> assertEquals("", Preferences.getString("checkedValueString", "")), - () -> assertEquals(0, Preferences.getInt("checkedValueInt", 0)), - () -> assertEquals(0, Preferences.getFloat("checkedValueFloat", 0), 1e-6), - () -> assertTrue(Preferences.getBoolean("checkedValueBoolean", true))); - } - @Nested class PutGetTests { @Test diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/NetworkBooleanEventTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/NetworkBooleanEventTest.java new file mode 100644 index 0000000000..045e05a2f3 --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/event/NetworkBooleanEventTest.java @@ -0,0 +1,47 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.wpilibj.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import edu.wpi.first.networktables.NetworkTableInstance; +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 NetworkBooleanEventTest { + NetworkTableInstance m_inst; + + @BeforeEach + void setup() { + m_inst = NetworkTableInstance.create(); + m_inst.startLocal(); + } + + @AfterEach + void teardown() { + m_inst.close(); + } + + @Test + void testNetworkBooleanEvent() { + var loop = new EventLoop(); + var counter = new AtomicInteger(0); + + var pub = m_inst.getTable("TestTable").getBooleanTopic("Test").publish(); + + new NetworkBooleanEvent(loop, m_inst, "TestTable", "Test").ifHigh(counter::incrementAndGet); + pub.set(false); + loop.poll(); + assertEquals(0, counter.get()); + pub.set(true); + loop.poll(); + assertEquals(1, counter.get()); + pub.set(false); + loop.poll(); + assertEquals(1, counter.get()); + } +} diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapperTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapperTest.java index 15f0b0b8ca..ed24453cba 100644 --- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapperTest.java +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/SendableCameraWrapperTest.java @@ -8,20 +8,22 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import edu.wpi.first.networktables.NetworkTableInstance; -import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SendableCameraWrapperTest { + NetworkTableInstance m_inst; + @BeforeEach void setup() { - NetworkTableInstance.getDefault().deleteAllEntries(); + m_inst = NetworkTableInstance.create(); SendableCameraWrapper.clearWrappers(); } - @AfterAll - static void tearDown() { - NetworkTableInstance.getDefault().deleteAllEntries(); + @AfterEach + void tearDown() { + m_inst.close(); } @Test diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java index 5dfad6796d..53369f10d4 100644 --- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/shuffleboard/ShuffleboardInstanceTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import edu.wpi.first.networktables.GenericEntry; import edu.wpi.first.networktables.NetworkTableEntry; import edu.wpi.first.networktables.NetworkTableInstance; import org.junit.jupiter.api.AfterEach; @@ -32,7 +33,7 @@ class ShuffleboardInstanceTest { @Test void testPathFluent() { - NetworkTableEntry entry = + GenericEntry entry = m_shuffleboardInstance .getTab("Tab Title") .getLayout("Layout Title", "List Layout") @@ -45,13 +46,13 @@ class ShuffleboardInstanceTest { () -> assertEquals( "/Shuffleboard/Tab Title/Layout Title/Data", - entry.getName(), + entry.getTopic().getName(), "Entry path generated incorrectly")); } @Test void testNestedLayoutsFluent() { - NetworkTableEntry entry = + GenericEntry entry = m_shuffleboardInstance .getTab("Tab") .getLayout("First", "List") @@ -66,7 +67,7 @@ class ShuffleboardInstanceTest { () -> assertEquals( "/Shuffleboard/Tab/First/Second/Third/Fourth/Value", - entry.getName(), + entry.getTopic().getName(), "Entry path generated incorrectly")); } @@ -78,14 +79,14 @@ class ShuffleboardInstanceTest { ShuffleboardLayout third = second.getLayout("Third", "List"); ShuffleboardLayout fourth = third.getLayout("Fourth", "List"); SimpleWidget widget = fourth.add("Value", "string"); - NetworkTableEntry entry = widget.getEntry(); + GenericEntry entry = widget.getEntry(); assertAll( () -> assertEquals("string", entry.getString(null), "Wrong entry value"), () -> assertEquals( "/Shuffleboard/Tab/First/Second/Third/Fourth/Value", - entry.getName(), + entry.getTopic().getName(), "Entry path generated incorrectly")); } diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java index dfefddb890..9d2b87c3ef 100644 --- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/smartdashboard/SmartDashboardTest.java @@ -10,11 +10,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.wpilibj.UtilityClassTest; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SmartDashboardTest extends UtilityClassTest { - private final NetworkTable m_table = NetworkTableInstance.getDefault().getTable("SmartDashboard"); + private NetworkTableInstance m_inst; + private NetworkTable m_table; SmartDashboardTest() { super(SmartDashboard.class); @@ -22,7 +24,14 @@ class SmartDashboardTest extends UtilityClassTest { @BeforeEach void beforeEach() { - m_table.getKeys().forEach(m_table::delete); + m_inst = NetworkTableInstance.create(); + m_table = m_inst.getTable("SmartDashboard"); + SmartDashboard.setNetworkTableInstance(m_inst); + } + + @AfterEach + void afterEach() { + m_inst.close(); } @Test diff --git a/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini b/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini deleted file mode 100644 index f271944795..0000000000 --- a/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.ini +++ /dev/null @@ -1,8 +0,0 @@ -[NetworkTables Storage 3.0] -double "/Preferences/checkedValueInt"=2 -; The omission of a leading zero is intentional for testing purposes -double "/Preferences/checkedValueDouble"=.2 -double "/Preferences/checkedValueFloat"=3.4 -double "/Preferences/checkedValueLong"=172 -string "/Preferences/checkedValueString"="Hello. How are you?" -boolean "/Preferences/checkedValueBoolean"=false diff --git a/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.json b/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.json new file mode 100644 index 0000000000..0d02b57876 --- /dev/null +++ b/wpilibj/src/test/resources/edu/wpi/first/wpilibj/PreferencesTestDefault.json @@ -0,0 +1,50 @@ +[ + { + "name": "/Preferences/checkedValueInt", + "type": "int", + "value": 2, + "properties": { + "persistent": true + } + }, + { + "name": "/Preferences/checkedValueDouble", + "type": "double", + "value": 0.2, + "properties": { + "persistent": true + } + }, + { + "name": "/Preferences/checkedValueFloat", + "type": "float", + "value": 3.4, + "properties": { + "persistent": true + } + }, + { + "name": "/Preferences/checkedValueLong", + "type": "int", + "value": 172, + "properties": { + "persistent": true + } + }, + { + "name": "/Preferences/checkedValueString", + "type": "string", + "value": "Hello. How are you?", + "properties": { + "persistent": true + } + }, + { + "name": "/Preferences/checkedValueBoolean", + "type": "boolean", + "value": false, + "properties": { + "persistent": true + } + } +] diff --git a/wpilibjExamples/build.gradle b/wpilibjExamples/build.gradle index 7a3a143030..45d56f2316 100644 --- a/wpilibjExamples/build.gradle +++ b/wpilibjExamples/build.gradle @@ -111,9 +111,9 @@ model { lib project: ':wpilibc', library: 'wpilibc', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' lib project: ':wpimath', library: 'wpimathJNI', linkage: 'shared' - lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + project(':ntcore').addNtcoreDependency(binary, 'shared') + project(':ntcore').addNtcoreJniDependency(binary) lib project: ':cscore', library: 'cscore', linkage: 'shared' - lib project: ':ntcore', library: 'ntcoreJNIShared', linkage: 'shared' lib project: ':cscore', library: 'cscoreJNIShared', linkage: 'shared' project(':hal').addHalDependency(binary, 'shared') project(':hal').addHalJniDependency(binary) diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Robot.java index eb6a6d94c8..f9991f6132 100644 --- a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Robot.java +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/shuffleboard/Robot.java @@ -4,7 +4,7 @@ package edu.wpi.first.wpilibj.examples.shuffleboard; -import edu.wpi.first.networktables.NetworkTableEntry; +import edu.wpi.first.networktables.GenericEntry; import edu.wpi.first.wpilibj.AnalogPotentiometer; import edu.wpi.first.wpilibj.Encoder; import edu.wpi.first.wpilibj.TimedRobot; @@ -22,7 +22,7 @@ public class Robot extends TimedRobot { private final PWMSparkMax m_elevatorMotor = new PWMSparkMax(2); private final AnalogPotentiometer m_elevatorPot = new AnalogPotentiometer(0); - private NetworkTableEntry m_maxSpeed; + private GenericEntry m_maxSpeed; @Override public void robotInit() { diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/simpledifferentialdrivesimulation/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/simpledifferentialdrivesimulation/Robot.java index a9691f58ba..1beccce57b 100644 --- a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/simpledifferentialdrivesimulation/Robot.java +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/simpledifferentialdrivesimulation/Robot.java @@ -32,10 +32,6 @@ public class Robot extends TimedRobot { @Override public void robotInit() { - // Flush NetworkTables every loop. This ensures that robot pose and other values - // are sent during every iteration. - setNetworkTablesFlushEnabled(true); - m_trajectory = TrajectoryGenerator.generateTrajectory( new Pose2d(2, 2, new Rotation2d()), diff --git a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/statespacedifferentialdrivesimulation/Robot.java b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/statespacedifferentialdrivesimulation/Robot.java index 75700c4a22..8eca311821 100644 --- a/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/statespacedifferentialdrivesimulation/Robot.java +++ b/wpilibjExamples/src/main/java/edu/wpi/first/wpilibj/examples/statespacedifferentialdrivesimulation/Robot.java @@ -26,10 +26,6 @@ public class Robot extends TimedRobot { // Instantiate our RobotContainer. This will perform all our button bindings, and put our // autonomous chooser on the dashboard. m_robotContainer = new RobotContainer(); - - // Flush NetworkTables every loop. This ensures that robot pose and other values - // are sent during every loop iteration. - setNetworkTablesFlushEnabled(true); } @Override diff --git a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java index 35d98fa3f3..aa0a9b064d 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableBuilder.java @@ -5,13 +5,17 @@ package edu.wpi.first.util.sendable; import edu.wpi.first.util.function.BooleanConsumer; +import edu.wpi.first.util.function.FloatConsumer; +import edu.wpi.first.util.function.FloatSupplier; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.DoubleConsumer; import java.util.function.DoubleSupplier; +import java.util.function.LongConsumer; +import java.util.function.LongSupplier; import java.util.function.Supplier; -public interface SendableBuilder { +public interface SendableBuilder extends AutoCloseable { /** The backend kinds used for the sendable builder. */ enum BackendKind { kUnknown, @@ -51,6 +55,24 @@ public interface SendableBuilder { */ void addBooleanProperty(String key, BooleanSupplier getter, BooleanConsumer setter); + /** + * Add an integer property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + void addIntegerProperty(String key, LongSupplier getter, LongConsumer setter); + + /** + * Add a float property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + void addFloatProperty(String key, FloatSupplier getter, FloatConsumer setter); + /** * Add a double property. * @@ -78,6 +100,24 @@ public interface SendableBuilder { */ void addBooleanArrayProperty(String key, Supplier getter, Consumer setter); + /** + * Add an integer array property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + void addIntegerArrayProperty(String key, Supplier getter, Consumer setter); + + /** + * Add a float array property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + void addFloatArrayProperty(String key, Supplier getter, Consumer setter); + /** * Add a double array property. * @@ -100,10 +140,12 @@ public interface SendableBuilder { * Add a raw property. * * @param key property name + * @param typeString type string * @param getter getter function (returns current value) * @param setter setter function (sets new value) */ - void addRawProperty(String key, Supplier getter, Consumer setter); + void addRawProperty( + String key, String typeString, Supplier getter, Consumer setter); /** * Gets the kind of backend being used. @@ -124,4 +166,11 @@ public interface SendableBuilder { /** Clear properties. */ void clearProperties(); + + /** + * Adds a closeable. The closeable.close() will be called when close() is called. + * + * @param closeable closeable object + */ + void addCloseable(AutoCloseable closeable); } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java index 944b3c2fe4..6f0ad41d65 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/sendable/SendableRegistry.java @@ -17,21 +17,32 @@ import java.util.function.Supplier; * The SendableRegistry class is the public interface for registering sensors and actuators for use * on dashboards and LiveWindow. */ +@SuppressWarnings("PMD.AvoidCatchingGenericException") public final class SendableRegistry { - private static class Component { + private static class Component implements AutoCloseable { Component() {} Component(Sendable sendable) { m_sendable = new WeakReference<>(sendable); } + @Override + public void close() throws Exception { + m_builder.close(); + for (AutoCloseable data : m_data) { + if (data != null) { + data.close(); + } + } + } + WeakReference m_sendable; SendableBuilder m_builder; String m_name; String m_subsystem = "Ungrouped"; WeakReference m_parent; boolean m_liveWindow; - Object[] m_data; + AutoCloseable[] m_data; void setName(String moduleType, int channel) { m_name = moduleType + "[" + channel + "]"; @@ -131,6 +142,13 @@ public final class SendableRegistry { public static synchronized void addLW(Sendable sendable, String name) { Component comp = getOrAdd(sendable); if (liveWindowFactory != null) { + if (comp.m_builder != null) { + try { + comp.m_builder.close(); + } catch (Exception e) { + // ignore + } + } comp.m_builder = liveWindowFactory.get(); } comp.m_liveWindow = true; @@ -147,6 +165,13 @@ public final class SendableRegistry { public static synchronized void addLW(Sendable sendable, String moduleType, int channel) { Component comp = getOrAdd(sendable); if (liveWindowFactory != null) { + if (comp.m_builder != null) { + try { + comp.m_builder.close(); + } catch (Exception e) { + // ignore + } + } comp.m_builder = liveWindowFactory.get(); } comp.m_liveWindow = true; @@ -165,6 +190,13 @@ public final class SendableRegistry { Sendable sendable, String moduleType, int moduleNumber, int channel) { Component comp = getOrAdd(sendable); if (liveWindowFactory != null) { + if (comp.m_builder != null) { + try { + comp.m_builder.close(); + } catch (Exception e) { + // ignore + } + } comp.m_builder = liveWindowFactory.get(); } comp.m_liveWindow = true; @@ -181,6 +213,13 @@ public final class SendableRegistry { public static synchronized void addLW(Sendable sendable, String subsystem, String name) { Component comp = getOrAdd(sendable); if (liveWindowFactory != null) { + if (comp.m_builder != null) { + try { + comp.m_builder.close(); + } catch (Exception e) { + // ignore + } + } comp.m_builder = liveWindowFactory.get(); } comp.m_liveWindow = true; @@ -211,7 +250,15 @@ public final class SendableRegistry { * @return true if the object was removed; false if it was not present */ public static synchronized boolean remove(Sendable sendable) { - return components.remove(sendable) != null; + Component comp = components.remove(sendable); + if (comp != null) { + try { + comp.close(); + } catch (Exception e) { + // ignore + } + } + return comp != null; } /** @@ -338,22 +385,33 @@ public final class SendableRegistry { * @param sendable object * @param handle data handle returned by getDataHandle() * @param data data to set - * @return Previous data (may be null) + * @return Previous data (may be null). If non-null, caller is responsible for calling close(). */ - public static synchronized Object setData(Sendable sendable, int handle, Object data) { + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public static synchronized AutoCloseable setData( + Sendable sendable, int handle, AutoCloseable data) { Component comp = components.get(sendable); if (comp == null) { return null; } - Object rv = null; + AutoCloseable rv = null; if (comp.m_data == null) { - comp.m_data = new Object[handle + 1]; + comp.m_data = new AutoCloseable[handle + 1]; } else if (handle < comp.m_data.length) { rv = comp.m_data[handle]; } else { comp.m_data = Arrays.copyOf(comp.m_data, handle + 1); } - comp.m_data[handle] = data; + if (comp.m_data[handle] != data) { + if (comp.m_data[handle] != null) { + try { + comp.m_data[handle].close(); + } catch (Exception e) { + // ignore + } + } + comp.m_data[handle] = data; + } return rv; } @@ -405,7 +463,11 @@ public final class SendableRegistry { public static synchronized void publish(Sendable sendable, SendableBuilder builder) { Component comp = getOrAdd(sendable); if (comp.m_builder != null) { - comp.m_builder.clearProperties(); + try { + comp.m_builder.close(); + } catch (Exception e) { + // ignore + } } comp.m_builder = builder; // clear any current builder sendable.initSendable(comp.m_builder); @@ -440,7 +502,7 @@ public final class SendableRegistry { public Sendable parent; /** Data stored in object with setData(). Update this to change the data. */ - public Object data; + public AutoCloseable data; /** Sendable builder for the sendable. */ public SendableBuilder builder; @@ -457,6 +519,7 @@ public final class SendableRegistry { * @param dataHandle data handle to get data object passed to callback * @param callback function to call for each object */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") public static synchronized void foreachLiveWindow( int dataHandle, Consumer callback) { CallbackData cbdata = new CallbackData(); @@ -494,11 +557,20 @@ public final class SendableRegistry { } if (cbdata.data != null) { if (comp.m_data == null) { - comp.m_data = new Object[dataHandle + 1]; + comp.m_data = new AutoCloseable[dataHandle + 1]; } else if (dataHandle >= comp.m_data.length) { comp.m_data = Arrays.copyOf(comp.m_data, dataHandle + 1); } - comp.m_data[dataHandle] = cbdata.data; + if (comp.m_data[dataHandle] != cbdata.data) { + if (comp.m_data[dataHandle] != null) { + try { + comp.m_data[dataHandle].close(); + } catch (Exception e) { + // ignore + } + } + comp.m_data[dataHandle] = cbdata.data; + } } } } diff --git a/wpiutil/src/main/native/include/wpi/Synchronization.h b/wpiutil/src/main/native/include/wpi/Synchronization.h index 57fc4eeca0..b2aaac88a5 100644 --- a/wpiutil/src/main/native/include/wpi/Synchronization.h +++ b/wpiutil/src/main/native/include/wpi/Synchronization.h @@ -43,8 +43,8 @@ constexpr int kHandleTypeEvent = 1; constexpr int kHandleTypeSemaphore = 2; constexpr int kHandleTypeCSBase = 3; constexpr int kHandleTypeNTBase = 16; -constexpr int kHandleTypeHALBase = 32; -constexpr int kHandleTypeUserBase = 64; +constexpr int kHandleTypeHALBase = 48; +constexpr int kHandleTypeUserBase = 80; /** @} */ /** diff --git a/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h b/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h index e092a1ef50..8b2c304122 100644 --- a/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h +++ b/wpiutil/src/main/native/include/wpi/sendable/SendableBuilder.h @@ -59,6 +59,28 @@ class SendableBuilder { std::function getter, std::function setter) = 0; + /** + * Add an integer property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + virtual void AddIntegerProperty(std::string_view key, + std::function getter, + std::function setter) = 0; + + /** + * Add a float property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + virtual void AddFloatProperty(std::string_view key, + std::function getter, + std::function setter) = 0; + /** * Add a double property. * @@ -92,6 +114,28 @@ class SendableBuilder { std::string_view key, std::function()> getter, std::function)> setter) = 0; + /** + * Add an integer array property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + virtual void AddIntegerArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) = 0; + + /** + * Add a float array property. + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + virtual void AddFloatArrayProperty( + std::string_view key, std::function()> getter, + std::function)> setter) = 0; + /** * Add a double array property. * @@ -117,13 +161,15 @@ class SendableBuilder { /** * Add a raw property. * - * @param key property name - * @param getter getter function (returns current value) - * @param setter setter function (sets new value) + * @param key property name + * @param typeString type string + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) */ - virtual void AddRawProperty(std::string_view key, - std::function getter, - std::function setter) = 0; + virtual void AddRawProperty( + std::string_view key, std::string_view typeString, + std::function()> getter, + std::function)> setter) = 0; /** * Add a string property (SmallString form). @@ -150,6 +196,33 @@ class SendableBuilder { getter, std::function)> setter) = 0; + /** + * Add an integer array property (SmallVector form). + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + virtual void AddSmallIntegerArrayProperty( + std::string_view key, + std::function< + wpi::span(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) = 0; + + /** + * Add a float array property (SmallVector form). + * + * @param key property name + * @param getter getter function (returns current value) + * @param setter setter function (sets new value) + */ + virtual void AddSmallFloatArrayProperty( + std::string_view key, + std::function(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) = 0; + /** * Add a double array property (SmallVector form). * @@ -181,13 +254,15 @@ class SendableBuilder { * Add a raw property (SmallVector form). * * @param key property name + * @param typeString type string * @param getter getter function (returns current value) * @param setter setter function (sets new value) */ virtual void AddSmallRawProperty( - std::string_view key, - std::function& buf)> getter, - std::function setter) = 0; + std::string_view key, std::string_view typeString, + std::function(wpi::SmallVectorImpl& buf)> + getter, + std::function)> setter) = 0; /** * Gets the kind of backend being used.