Design pattern for component model signal registration in Icarus, using Vulcan's signal primitives as the wire format foundation.
Overview
Every Icarus component model registers its I/O interface during initialization. The Signal Service provides a unified API for:
| Registration | Purpose |
| Parameters (int) | Configuration values, modes, counters |
| Parameters (double) | Physical constants, tuning values |
| Input signals | Data consumed from other components |
| Output signals | Data produced for other components / telemetry |
All signals use Vulcan's SignalType and SignalLifecycle for wire format compatibility.
Component Init Pattern
#include <icarus/SignalService.hpp>
class RocketDynamics : public ComponentModel {
public:
void init(SignalService& signals) override {
signals.bind_param("num_engines", &num_engines_)
.unit("count")
.info("Number of active engines");
signals.bind_param("guidance_mode", &guidance_mode_)
.semantic("enum")
.info("0=inertial, 1=terminal, 2=landing");
signals.bind_param("dry_mass", &dry_mass_)
.unit("kg")
.info("Vehicle dry mass excluding propellant");
signals.bind_param("isp_vacuum", &isp_vacuum_)
.unit("s")
.info("Specific impulse in vacuum");
signals.bind_input("thrust_body", &thrust_body_)
.unit("N")
.info("Thrust vector in body frame");
signals.bind_input("throttle", &throttle_)
.unit("ratio")
.info("Throttle command [0, 1]");
signals.bind_input("attitude_cmd", &attitude_cmd_);
signals.bind_output("position_ecef", &position_)
.unit("m")
.info("Position in ECEF frame");
signals.bind_output("velocity_ecef", &velocity_)
.unit("m/s");
signals.bind_output("attitude", &attitude_);
signals.bind_output("altitude_agl", &altitude_agl_)
.unit("m")
.info("Altitude above ground level");
signals.bind_output("flight_phase", &flight_phase_)
.semantic("enum")
.info("0=prelaunch, 1=powered, 2=coast, 3=terminal");
}
void step(double dt) override {
Eigen::Vector3d accel = thrust_body_ * throttle_ / mass();
velocity_ += accel * dt;
position_ += velocity_ * dt;
}
private:
int32_t num_engines_ = 9;
int32_t guidance_mode_ = 0;
double dry_mass_ = 22200.0;
double isp_vacuum_ = 311.0;
Eigen::Vector3d thrust_body_ = Eigen::Vector3d::Zero();
double throttle_ = 0.0;
Eigen::Quaterniond attitude_cmd_ = Eigen::Quaterniond::Identity();
Eigen::Vector3d position_ = Eigen::Vector3d::Zero();
Eigen::Vector3d velocity_ = Eigen::Vector3d::Zero();
Eigen::Quaterniond attitude_ = Eigen::Quaterniond::Identity();
double altitude_agl_ = 0.0;
int32_t flight_phase_ = 0;
};
Core signal types for telemetry and streaming.
Signal Lifecycle Mapping
| Registration Type | SignalLifecycle | Wire Behavior |
| add_param_* | Static | Sent once at handshake |
| add_input_* | Dynamic | Updated each tick |
| add_output_* | Dynamic | Streamed at 60Hz |
SignalService API
namespace icarus {
class SignalBuilder {
public:
SignalBuilder& unit(std::string_view u);
SignalBuilder& semantic(std::string_view s);
SignalBuilder& info(std::string_view description);
};
class SignalService {
public:
SignalBuilder& bind_param(std::string_view name, int32_t* ptr);
SignalBuilder& bind_param(std::string_view name, int64_t* ptr);
SignalBuilder& bind_param(std::string_view name, double* ptr);
SignalBuilder& bind_input(std::string_view name, double* ptr);
SignalBuilder& bind_input(std::string_view name, int32_t* ptr);
SignalBuilder& bind_input(std::string_view name, Eigen::Vector3d* ptr);
SignalBuilder& bind_input(std::string_view name, Eigen::Quaterniond* ptr);
SignalBuilder& bind_output(std::string_view name, double* ptr);
SignalBuilder& bind_output(std::string_view name, int32_t* ptr);
SignalBuilder& bind_output(std::string_view name, Eigen::Vector3d* ptr);
SignalBuilder& bind_output(std::string_view name, Eigen::Quaterniond* ptr);
void propagate_inputs();
std::string to_json() const;
};
}
Single timestep of telemetry data.
Definition Frame.hpp:39
Telemetry schema builder and container.
Definition TelemetrySchema.hpp:41
Data Flow Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ ICARUS SIMULATION │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Guidance │────▶│ Dynamics │────▶│ Sensors │ │
│ │ Component │ │ Component │ │ Component │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ SIGNAL SERVICE (Data Backplane) │ │
│ │ • Routes signals between components │ │
│ │ • Maintains TelemetrySchema from all registrations │ │
│ │ • Provides Frame buffer for current timestep │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────────────┼────────────────────────────────────────┘
│
┌────────────────┴────────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ VULCAN HDF5 │ │ HERMES │
│ (Post-process)│ │ (Real-time) │
└───────────────┘ └───────────────┘
Wire Format Compatibility
The SignalService internally translates registrations to Vulcan signals:
signals.add_output_vec3("position_ecef", "m");
schema.add_vec3("rocket.position_ecef", SignalLifecycle::Dynamic, "m");
Component names are prefixed automatically to avoid collisions.
Connection Validation
During simulation init, the SignalService validates that:
- Every input has a matching output from some component
- No duplicate signal names exist
- Type compatibility between connected signals
void Simulation::init() {
for (auto& component : components_) {
component->init(signal_service_);
}
signal_service_.validate_connections();
telemetry_schema_ = signal_service_.to_schema();
}
Telemetry Selection
Not all signals need to be streamed. The SignalService supports filtering:
auto telemetry_schema = signal_service_.to_schema(
SignalFilter::OutputsOnly
);
auto filtered = signal_service_.to_schema(
SignalFilter::ByPattern("*.position_*")
);
See Also