The Vulcan Data I/O module provides a comprehensive telemetry system for recording simulation data, post-processing analysis, and real-time streaming to the Hermes middleware.
Overview
The I/O module consists of:
| Component | Purpose |
| TelemetrySchema | Defines signal names, types, and lifecycle |
| Frame | Holds a single timestep of data |
| HDF5Writer | Writes frames to HDF5 files |
| HDF5Reader | Reads HDF5 files for analysis |
| CSVExport | Exports HDF5 data to CSV |
| FrameSerializer | Binary serialization for real-time streaming |
Quick Start
for (double t = 0; t < 10.0; t += 0.01) {
frame.set_time(t);
frame.set("position", compute_position(t));
frame.set("velocity", compute_velocity(t));
frame.set("phase", int32_t{1});
frame.set("mass", 1000.0);
writer.write_frame(frame);
}
auto positions = reader.read_vec3("position");
auto times = reader.times();
Single timestep of telemetry data.
Definition Frame.hpp:39
HDF5 telemetry file reader.
Definition HDF5Reader.hpp:41
HDF5 telemetry file writer.
Definition HDF5Writer.hpp:56
Telemetry schema builder and container.
Definition TelemetrySchema.hpp:41
TelemetrySchema & add_int32(const std::string &name, SignalLifecycle lifecycle=SignalLifecycle::Dynamic, const std::string &semantic="")
Add an int32 signal.
Definition TelemetrySchema.hpp:72
TelemetrySchema & add_double(const std::string &name, SignalLifecycle lifecycle=SignalLifecycle::Dynamic, const std::string &unit="")
Add a double signal.
Definition TelemetrySchema.hpp:57
TelemetrySchema & add_vec3(const std::string &name, SignalLifecycle lifecycle=SignalLifecycle::Dynamic, const std::string &unit="")
Add a 3-component vector (expands to name.x, name.y, name.z).
Definition TelemetrySchema.hpp:104
Definition CSVExport.hpp:20
@ Static
Constant after init, sent in schema handshake only.
Definition Signal.hpp:26
@ Dynamic
Updated each step, included in streaming telemetry.
Definition Signal.hpp:27
Telemetry Schema
Signal Types
All signals are stored as 8-byte aligned values:
| Type | Wire Size | Description |
| Double | 8 bytes | 64-bit floating point |
| Int32 | 8 bytes | 32-bit integer (padded) |
| Int64 | 8 bytes | 64-bit integer |
Signal Lifecycle
| Lifecycle | Description | Streaming Behavior |
| Dynamic | Changes every frame | Sent at 60Hz |
| Static | Set once at startup | Sent once in handshake |
Adding Signals
TelemetrySchema & add_int64(const std::string &name, SignalLifecycle lifecycle=SignalLifecycle::Dynamic)
Add an int64 signal.
Definition TelemetrySchema.hpp:86
TelemetrySchema & add_quat(const std::string &name, SignalLifecycle lifecycle=SignalLifecycle::Dynamic)
Add a quaternion (expands to name.w, name.x, name.y, name.z).
Definition TelemetrySchema.hpp:120
Schema Introspection
std::cout <<
"Signal count: " << schema.
signal_count() <<
"\n";
const auto& sig = schema.
signal(
"position.x");
std::cout <<
"Unit: " << sig.
unit <<
"\n";
}
std::cout << sig.name << " (dynamic)\n";
}
bool has_signal(const std::string &name) const
Check if signal exists.
Definition TelemetrySchema.hpp:168
size_t signal_count() const
Number of signals.
Definition TelemetrySchema.hpp:210
const SignalDescriptor & signal(const std::string &name) const
Get signal by name.
Definition TelemetrySchema.hpp:159
std::vector< SignalDescriptor > dynamic_signals() const
Get only dynamic signals.
Definition TelemetrySchema.hpp:137
size_t frame_size_bytes() const
Total frame size in bytes (all signals).
Definition TelemetrySchema.hpp:185
@ Double
8 bytes - physical quantities
Definition Signal.hpp:19
std::string unit
Physical unit (e.g., "m", "rad/s").
Definition Signal.hpp:35
Frame
The Frame class holds one timestep of telemetry data:
frame.set_time(1.234);
frame.set("altitude", 10000.0);
frame.set("phase", int32_t{2});
frame.set("position", Eigen::Vector3d(1000, 2000, 3000));
frame.set("velocity", Eigen::Vector3d(100, 200, 300));
frame.set("attitude", Eigen::Quaterniond::Identity());
double alt = frame.get_double("altitude");
int32_t phase = frame.get_int32("phase");
Eigen::Vector3d pos = frame.get_vec3("position");
Eigen::Quaterniond att = frame.get_quat("attitude");
frame.clear();
HDF5 Writer
Records frames to HDF5 files with chunked, extensible datasets:
for (size_t i = 0; i < 10000; ++i) {
frame.set_time(i * 0.001);
writer.write_frame(frame);
}
std::cout << "Frames written: " << writer.frame_count() << "\n";
writer.flush();
writer.close();
HDF5 File Structure
/time # (N,) float64 - timestamps
/signals/
position.x # (N,) float64
position.y # (N,) float64
position.z # (N,) float64
phase # (N,) int32
mass # (N,) float64
/metadata/
schema # JSON string attribute
created_at # ISO 8601 timestamp
HDF5 Reader
Reads HDF5 files for post-simulation analysis:
std::cout << "Frames: " << reader.frame_count() << "\n";
auto times = reader.times();
auto altitudes = reader.read_double("altitude");
auto phases = reader.read_int32("phase");
auto positions = reader.read_vec3("position");
auto attitudes = reader.read_quat("attitude");
auto slice = reader.read_double("altitude", 1000, 100);
for (const auto& name : reader.signal_names()) {
std::cout << name << "\n";
}
auto schema = reader.schema();
CSV Export
Export HDF5 data to CSV for external tools:
options.
signals = {
"position.x",
"position.y",
"position.z"};
std::stringstream ss;
void export_to_csv(const HDF5Reader &reader, std::ostream &out, const CSVExportOptions &options={})
Export HDF5 reader to output stream.
Definition CSVExport.hpp:36
CSV export options.
Definition CSVExport.hpp:23
int precision
Decimal precision for doubles.
Definition CSVExport.hpp:25
char delimiter
Column delimiter.
Definition CSVExport.hpp:24
std::vector< std::string > signals
Signal subset (empty = all).
Definition CSVExport.hpp:27
bool include_header
Include column names in first row.
Definition CSVExport.hpp:26
Binary Serialization (Hermes Integration)
The FrameSerializer provides efficient binary serialization for real-time streaming to the Hermes middleware:
std::cout << "Dynamic frame: " << serializer.dynamic_size_bytes() << " bytes\n";
std::cout << "Static data: " << serializer.static_size_bytes() << " bytes\n";
frame.set_time(1.0);
std::span<const std::byte> dynamic_bytes = serializer.serialize(frame);
std::span<const std::byte> static_bytes = serializer.serialize_statics(frame);
hermes_buffer.write(dynamic_bytes.data(), dynamic_bytes.size());
serializer.deserialize_statics(static_bytes, received);
serializer.deserialize(dynamic_bytes, received);
Binary frame serializer for Hermes real-time streaming.
Definition FrameSerializer.hpp:54
Wire Format
All values are little-endian, 8-byte aligned:
Dynamic Frame:
+------------------+------------------+------------------+
| time (8 bytes) | signal_0 (8B) | signal_1 (8B)... |
+------------------+------------------+------------------+
Static Frame:
+------------------+------------------+
| static_0 (8B) | static_1 (8B)... |
+------------------+------------------+
JSON Schema (Protocol Negotiation)
The schema can be serialized to JSON for protocol negotiation:
std::string json = schema.
to_json();
static TelemetrySchema from_json(const std::string &json)
Deserialize from JSON string (basic parser).
std::string to_json() const
Serialize to JSON string.
Definition TelemetrySchema.hpp:240
Example JSON output:
{
"signals": [
{"name": "position.x", "type": "double", "lifecycle": "dynamic", "unit": "m"},
{"name": "position.y", "type": "double", "lifecycle": "dynamic", "unit": "m"},
{"name": "position.z", "type": "double", "lifecycle": "dynamic", "unit": "m"},
{"name": "phase", "type": "int32", "lifecycle": "dynamic", "semantic": "enum"},
{"name": "mass", "type": "double", "lifecycle": "static", "unit": "kg"}
]
}
Example
See the complete demo: telemetry_demo.cpp
# Build and run
./scripts/build.sh
./build/examples/telemetry_demo