@Amicus1 summarized it well. I need to write a docs page on it, but essentially structured data serialization closes a gap in telemetry/datalogging for complex data types. Prior to adding this feature, you essentially had the following options for sending a complex piece of data such as a Pose2d
(or Pose2d[]
) over NetworkTables (or datalogging it):
- Split it into separate topics (e.g. a topic for X, a topic for Y, and a topic for Rotation) and send updates as separate values/arrays
- Flatten it into a
double[]
with an agreed-upon but essentially undocumented order (e.g. each Pose2d would be 3 doubles in the array, in X, Y, Rotation order) - Serializing to a text format such as JSON
- Manually serializing to raw bytes
Each of these have significant disadvantages.
- Multiple topics: NT makes no guarantees of when updates from two separate topics are updated relative to each other, so the first one risks data slicing–essentially reading X from one pose and Y from a different pose, or with an array, having those two arrays be different sizes. With timestamps etc you can disambiguate it but it’s a lot of manual work.
- Flattening to
double[]
: Works decently for things like Pose2d where all pieces are doubles, but is inefficient for mixed numeric types, and can’t handle non-numeric types at all. The biggest downside is the ordering must be previously agreed upon by both sender and receiver and there’s no way to “discover” what that ordering is. - Serializing to JSON: text is very inefficient both from a time and space perspective
- Manually serializing: works, but is a lot of manual work, and without standard support for communicating the encoding schema, has the same problem as flattening to
double[]
in that there’s no good discovery mechanism for pulling apart the data dynamically.
What struct and protobuf offer is a standardized way to serialize to raw bytes. The schema (the description of how the data is encoded) is also published/logged so tools can dynamically decode the encoded data. The data is sent as a single topic, so there’s no data slicing concern, and it’s binary so it’s fast and small.
Struct encodes fixed-size structures (similar to a C struct). Structures can be nested (e.g. a Pose2d struct can be comprised of a Translation2d struct and a Rotation2d struct). Internal arrays (within the struct) must be fixed size. Dynamic sized arrays are supported at the top level (e.g. you can send a Pose2d[]
). Structs are fast–we’ve benchmarked it at about half the speed of flattening to double[]
. The serialization implementation (at present) is manually written, but as everything is fixed size this isn’t too bad (just some ByteBuffer calls in Java).
Protobuf is much more powerful, but at a performance penalty–it’s about 1/6 the speed of flattening to double[]
. This is a Google-developed protocol that is quite common in industry. It offers several advantages over struct serialization, in that it allows variable-sized internal arrays and other dynamically sized elements (e.g. strings). The main disadvantage (other than speed) is that it doesn’t support dynamically sized arrays at the top level–if you want an array of Pose2d you have to create a protobuf that contains an array of Pose2d, and then send that. Serialization code is auto-generated from a .proto description file, but it’s still necessary to write some glue code to connect it to the actual object.
From a user perspective, the API is very straightforward. The idiom is for classes that natively support these serialization methods to include the glue code as static objects in the class implementation (.struct
for structs, and .proto
for protobufs), but it’s possible to write the glue code outside the object (e.g. to add support for arbitrary Java objects if desired).
For a struct array publisher to NetworkTables:
StructArrayTopic<Pose2d> topic = inst.getStructArrayTopic("poses", Pose2d.struct);
StructArrayPublisher<Pose2d> pub = topic.publish();
Pose2d[] arr = new Pose2d[] { new Pose2d(3, 4, new Rotation2d(0.1)), new Pose2d(5, 6, new Rotation2d(0.2)) };
pub.set(arr);
And similarly for a single Pose2d to protobuf (array isn’t supported at the top level):
ProtobufTopic<Pose2d> topic = inst.getProtobufTopic("pose", Pose2d.proto);
ProtobufPublisher<Pose2d> pub = topic.publish();
pub.set(new Pose2d(3, 4, 0.1));
The cool part about this is because the schema is communicated, the dashboard side can pull it apart completely dynamically: