|
| 1 | +--- |
| 2 | +icon: lucide/box |
| 3 | +--- |
| 4 | + |
| 5 | +# Struct Logging |
| 6 | + |
| 7 | +TelemetryKit supports WPILib struct serialization with automatic schema publishing to NetworkTables and WPILog files. |
| 8 | + |
| 9 | +!!! warning "Beta Feature" |
| 10 | + Struct logging is in beta. Some structs may not work correctly. Use the helper functions (e.g., `LogPose2d()`, `LogSwerveModuleStates()`) for more reliable decomposed logging. <!-- (1)! --> |
| 11 | + |
| 12 | +Helper functions decompose structs into individual primitive fields, avoiding serialization issues and providing better dashboard compatibility. |
| 13 | + |
| 14 | +## Quick Start |
| 15 | + |
| 16 | +```cpp |
| 17 | +#include <frc/geometry/Pose2d.h> |
| 18 | +#include <telemetrykit/TelemetryKit.h> |
| 19 | + |
| 20 | +// Log a Pose2d |
| 21 | +frc::Pose2d robotPose{1.5_m, 2.3_m, frc::Rotation2d{45_deg}}; |
| 22 | +tkit::Logger::RecordOutput("Odometry/Pose", tkit::MakeStructValue(robotPose)); |
| 23 | + |
| 24 | +// Log an array of SwerveModuleStates |
| 25 | +std::array<frc::SwerveModuleState, 4> moduleStates = {...}; |
| 26 | +tkit::Logger::RecordOutput("Drive/ModuleStates", |
| 27 | + tkit::MakeStructArrayValue(std::span(moduleStates))); |
| 28 | +``` |
| 29 | +
|
| 30 | +TelemetryKit automatically publishes struct schemas to NetworkTables and encodes them correctly in WPILog files. |
| 31 | +
|
| 32 | +## How It Works |
| 33 | +
|
| 34 | +### Serialization |
| 35 | +
|
| 36 | +When you call `MakeStructValue()`, TelemetryKit: |
| 37 | +
|
| 38 | +1. Serializes the struct to bytes using `wpi::Struct` |
| 39 | +2. Extracts the type name (e.g., "Pose2d") |
| 40 | +3. Collects schema bytes using `wpi::GetStructSchemaBytes<T>()` |
| 41 | +4. Gathers nested struct schemas (e.g., Translation2d, Rotation2d for Pose2d) |
| 42 | +
|
| 43 | +```mermaid |
| 44 | +graph LR |
| 45 | + A[Pose2d Object] -->|MakeStructValue| B[Serialize to bytes] |
| 46 | + B --> C[Extract type name] |
| 47 | + C --> D[Get schema bytes] |
| 48 | + D --> E[Collect nested schemas] |
| 49 | + E --> F[LogValue with all schemas] |
| 50 | +``` |
| 51 | + |
| 52 | +### Schema Publishing |
| 53 | + |
| 54 | +#### NetworkTables |
| 55 | + |
| 56 | +When `NetworkTablesReceiver` encounters a struct for the first time: |
| 57 | + |
| 58 | +```cpp |
| 59 | +// Publishes schemas for Pose2d and its nested structs |
| 60 | +m_inst.AddSchema("Pose2d", "structschema", pose2dSchemaBytes); |
| 61 | +m_inst.AddSchema("Translation2d", "structschema", translation2dSchemaBytes); |
| 62 | +m_inst.AddSchema("Rotation2d", "structschema", rotation2dSchemaBytes); |
| 63 | + |
| 64 | +// Then publishes the data as a RawTopic |
| 65 | +auto topic = m_inst.GetRawTopic("/Odometry/Pose"); |
| 66 | +auto publisher = topic.Publish("Pose2d"); // (1)! |
| 67 | +publisher.Set(serializedBytes, timestamp); |
| 68 | +``` |
| 69 | + |
| 70 | +1. The type string "Pose2d" tells NT4 clients which schema to use for deserialization |
| 71 | + |
| 72 | +#### WPILog Files |
| 73 | + |
| 74 | +`WPILogWriter` creates log entries with the correct type string: |
| 75 | + |
| 76 | +```cpp |
| 77 | +// Creates entry with "struct:Pose2d" type |
| 78 | +int entryId = m_log->Start("/Odometry/Pose", "struct:Pose2d"); |
| 79 | + |
| 80 | +// Appends serialized bytes |
| 81 | +m_log->AppendRaw(entryId, serializedBytes, timestamp); |
| 82 | +``` |
| 83 | +
|
| 84 | +Tools like AdvantageScope can then deserialize the struct data for visualization. |
| 85 | +
|
| 86 | +## Nested Struct Dependencies |
| 87 | +
|
| 88 | +TelemetryKit automatically handles nested struct dependencies using `wpi::ForEachStructSchema<T>()`: |
| 89 | +
|
| 90 | +```cpp |
| 91 | +// When you log a Pose2d |
| 92 | +tkit::MakeStructValue(pose); |
| 93 | +
|
| 94 | +// TelemetryKit collects schemas for: |
| 95 | +// - Pose2d (the main struct) |
| 96 | +// - Translation2d (nested in Pose2d) |
| 97 | +// - Rotation2d (nested in Pose2d) |
| 98 | +``` |
| 99 | + |
| 100 | +Without nested schemas, clients cannot deserialize structs that contain other structs. With nested schemas, clients receive all required schemas for full deserialization. |
| 101 | + |
| 102 | +## Supported Structs |
| 103 | + |
| 104 | +Any WPILib struct that implements `wpi::Struct<T>` is supported: |
| 105 | + |
| 106 | +### Geometry Structs |
| 107 | + |
| 108 | +```cpp |
| 109 | +#include <frc/geometry/Pose2d.h> |
| 110 | +#include <frc/geometry/Pose3d.h> |
| 111 | +#include <frc/geometry/Translation2d.h> |
| 112 | +#include <frc/geometry/Translation3d.h> |
| 113 | +#include <frc/geometry/Rotation2d.h> |
| 114 | +#include <frc/geometry/Rotation3d.h> |
| 115 | +#include <frc/geometry/Transform2d.h> |
| 116 | +#include <frc/geometry/Transform3d.h> |
| 117 | + |
| 118 | +tkit::Logger::RecordOutput("Pose2D", tkit::MakeStructValue(pose2d)); |
| 119 | +tkit::Logger::RecordOutput("Pose3D", tkit::MakeStructValue(pose3d)); |
| 120 | +``` |
| 121 | +
|
| 122 | +### Kinematics Structs |
| 123 | +
|
| 124 | +```cpp |
| 125 | +#include <frc/kinematics/SwerveModuleState.h> |
| 126 | +#include <frc/kinematics/SwerveModulePosition.h> |
| 127 | +#include <frc/kinematics/ChassisSpeeds.h> |
| 128 | +
|
| 129 | +tkit::Logger::RecordOutput("ModuleStates", |
| 130 | + tkit::MakeStructArrayValue(moduleStates)); |
| 131 | +``` |
| 132 | + |
| 133 | +### Custom Structs |
| 134 | + |
| 135 | +You can use any custom struct that implements `wpi::Struct<T>`: |
| 136 | + |
| 137 | +```cpp |
| 138 | +// Your custom struct with wpi::Struct implementation |
| 139 | +struct CustomData { |
| 140 | + double value1; |
| 141 | + int value2; |
| 142 | + |
| 143 | + using StructType = wpi::Struct<CustomData>; |
| 144 | + static constexpr std::string_view GetTypeName() { return "CustomData"; } |
| 145 | + // ... implement required methods |
| 146 | +}; |
| 147 | + |
| 148 | +CustomData data{1.5, 42}; |
| 149 | +tkit::Logger::RecordOutput("Custom", tkit::MakeStructValue(data)); |
| 150 | +``` |
| 151 | +
|
| 152 | +## Struct Helper Functions |
| 153 | +
|
| 154 | +For dashboard visibility, TelemetryKit provides helpers to decompose structs into individual fields: |
| 155 | +
|
| 156 | +```cpp |
| 157 | +#include <telemetrykit/core/StructLogger.h> |
| 158 | +
|
| 159 | +frc::Pose2d pose{1.5_m, 2.3_m, frc::Rotation2d{45_deg}}; |
| 160 | +
|
| 161 | +// Instead of logging as a binary struct |
| 162 | +tkit::Logger::RecordOutput("Pose", tkit::MakeStructValue(pose)); |
| 163 | +
|
| 164 | +// Decompose into individual fields for plotting |
| 165 | +auto& table = tkit::Logger::GetTable(); |
| 166 | +tkit::LogPose2d(table, "Pose", pose); |
| 167 | +// Creates: |
| 168 | +// "Pose/X" -> 1.5 |
| 169 | +// "Pose/Y" -> 2.3 |
| 170 | +// "Pose/Rotation" -> 0.785398 (45° in radians) |
| 171 | +``` |
| 172 | + |
| 173 | +Available helpers: |
| 174 | + |
| 175 | +| Function | Struct Type | Fields Created | |
| 176 | +|----------|-------------|----------------| |
| 177 | +| `LogPose2d` | `frc::Pose2d` | X, Y, Rotation | |
| 178 | +| `LogPose3d` | `frc::Pose3d` | X, Y, Z, Rotation (Quaternion) | |
| 179 | +| `LogTranslation2d` | `frc::Translation2d` | X, Y | |
| 180 | +| `LogTranslation3d` | `frc::Translation3d` | X, Y, Z | |
| 181 | +| `LogSwerveModuleState` | `frc::SwerveModuleState` | Speed, Angle | |
| 182 | +| `LogSwerveModulePosition` | `frc::SwerveModulePosition` | Distance, Angle | |
| 183 | +| `LogSwerveModuleStates` | `std::vector<...>` | Module0/..., Module1/..., etc. | |
| 184 | + |
| 185 | +Use structs for full type preservation in AdvantageScope. Use helpers for individual fields that can be plotted on dashboards. |
| 186 | + |
| 187 | +## Performance Considerations |
| 188 | + |
| 189 | +### Serialization Overhead |
| 190 | + |
| 191 | +Struct serialization has minimal overhead: |
| 192 | + |
| 193 | +```cpp |
| 194 | +frc::Pose2d pose{...}; |
| 195 | + |
| 196 | +// ~500 nanoseconds on roboRIO |
| 197 | +auto logValue = tkit::MakeStructValue(pose); // (1)! |
| 198 | +``` |
| 199 | +
|
| 200 | +1. Includes serialization, schema lookup, and LogValue construction |
| 201 | +
|
| 202 | +### Schema Publishing |
| 203 | +
|
| 204 | +Schemas are only published once per struct type: |
| 205 | +
|
| 206 | +```cpp |
| 207 | +// First Pose2d - publishes schemas |
| 208 | +tkit::Logger::RecordOutput("Pose1", tkit::MakeStructValue(pose1)); |
| 209 | +tkit::Logger::Periodic(); // → Publishes 3 schemas |
| 210 | +
|
| 211 | +// Second Pose2d - skips schema publishing |
| 212 | +tkit::Logger::RecordOutput("Pose2", tkit::MakeStructValue(pose2)); |
| 213 | +tkit::Logger::Periodic(); // → Only publishes data |
| 214 | +``` |
| 215 | + |
| 216 | +Schema publishing is a one-time cost per struct type, not per value. |
| 217 | + |
| 218 | +## Examples |
| 219 | + |
| 220 | +### Complete Swerve Drive Telemetry |
| 221 | + |
| 222 | +```cpp |
| 223 | +#include <telemetrykit/TelemetryKit.h> |
| 224 | +#include <telemetrykit/core/StructLogger.h> |
| 225 | +#include <frc/kinematics/SwerveModuleState.h> |
| 226 | + |
| 227 | +class SwerveDrive { |
| 228 | + public: |
| 229 | + void UpdateTelemetry() { |
| 230 | + auto& table = tkit::Logger::GetTable(); |
| 231 | + |
| 232 | + // Log robot pose (full struct for AdvantageScope) |
| 233 | + tkit::Logger::RecordOutput("Swerve/Pose", |
| 234 | + tkit::MakeStructValue(m_poseEstimator.GetEstimatedPosition())); |
| 235 | + |
| 236 | + // Log module states (decomposed for dashboard) |
| 237 | + tkit::LogSwerveModuleStates(table, "Swerve/ModuleStates", m_moduleStates); |
| 238 | + |
| 239 | + // Log as struct array for AdvantageScope |
| 240 | + tkit::Logger::RecordOutput("Swerve/ModuleStatesStruct", |
| 241 | + tkit::MakeStructArrayValue(std::span(m_moduleStates))); |
| 242 | + } |
| 243 | + |
| 244 | + private: |
| 245 | + std::array<frc::SwerveModuleState, 4> m_moduleStates; |
| 246 | + frc::SwerveDrivePoseEstimator m_poseEstimator; |
| 247 | +}; |
| 248 | +``` |
| 249 | +
|
| 250 | +This creates the following NT4 topics and log entries: |
| 251 | +
|
| 252 | +``` |
| 253 | +/Swerve/Pose → Pose2d struct |
| 254 | +/Swerve/ModuleStates/Module0/Speed → double |
| 255 | +/Swerve/ModuleStates/Module0/Angle → double |
| 256 | +/Swerve/ModuleStates/Module1/Speed → double |
| 257 | +/Swerve/ModuleStates/Module1/Angle → double |
| 258 | +... |
| 259 | +/Swerve/ModuleStatesStruct → SwerveModuleState[] struct array |
| 260 | +``` |
| 261 | +
|
| 262 | +--- |
| 263 | +
|
| 264 | +[Learn More About Receivers →](../receivers/overview.md){ .md-button } |
0 commit comments