[wpiutil, ntcore] Add structured data support (#5391)

This adds support for two serialization formats for complex data types:

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

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

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

Uses the Google protobuf implementation for C++ and the QuickBuffers alternative protobuf implementation for Java.
This commit is contained in:
Peter Johnson
2023-10-19 21:41:47 -07:00
committed by GitHub
parent ecb7cfa9ef
commit cf54d9ccb7
133 changed files with 13506 additions and 90 deletions

View File

@@ -7,6 +7,7 @@
#include <algorithm>
#include <wpi/DataLog.h>
#include <wpi/SmallString.h>
#include <wpi/StringExtras.h>
#include <wpi/json.h>
@@ -1481,6 +1482,41 @@ void LocalStorage::StopDataLog(NT_DataLogger logger) {
}
}
bool LocalStorage::HasSchema(std::string_view name) {
std::scoped_lock lock{m_mutex};
wpi::SmallString<128> fullName{"/.schema/"};
fullName += name;
auto it = m_impl.m_schemas.find(fullName);
return it != m_impl.m_schemas.end();
}
void LocalStorage::AddSchema(std::string_view name, std::string_view type,
std::span<const uint8_t> schema) {
std::scoped_lock lock{m_mutex};
wpi::SmallString<128> fullName{"/.schema/"};
fullName += name;
auto& pubHandle = m_impl.m_schemas[fullName];
if (pubHandle != 0) {
return;
}
auto topic = m_impl.GetOrCreateTopic(fullName);
if (topic->localPublishers.size() >= kMaxPublishers) {
WPI_ERROR(m_impl.m_logger,
"reached maximum number of publishers to '{}', not publishing",
topic->name);
return;
}
pubHandle = m_impl
.AddLocalPublisher(topic, {{"retained", true}},
PubSubConfig{NT_RAW, type, {}})
->handle;
m_impl.SetDefaultEntryValue(pubHandle, Value::MakeRaw(schema));
}
void LocalStorage::Reset() {
std::scoped_lock lock{m_mutex};
m_impl.m_network = nullptr;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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