Signal Registry Architecture
Overview
The Signal Registry pattern provides clean separation between physics simulation and device protocol implementation. Physics engine operates on abstract signal paths, devices expose protocol-specific interfaces.
Key Components
ISignalSource Interface
class ISignalSource {
virtual std::optional<double> read_signal(const std::string& path) = 0;
virtual void write_signal(const std::string& path, double value) = 0;
};
Contract: Path format is device_id/signal_id. Returns nullopt if signal unavailable.
SignalRegistry
Thread-safe cache implementing ISignalSource. Coordinates between physics ticker and device request handlers.
Key members:
signal_cache_- Cached signal values (map<string, double>)physics_driven_signals_- Signals managed by physics (set<string>)device_reader_- Callback to read actual device state (for actuators)registry_mutex_- Protects all operations
SimPhysics
Physics engine depends only on ISignalSource*. Zero knowledge of ADPP protocol.
Signal flow:
- Read actuator states via
signal_source_->read_signal() - Evaluate signal graph (transforms, routing)
- Update physics models
- Write sensor values via
signal_source_->write_signal()
Device Integration
Devices check registry before returning internal state:
// In read_signals():
std::string path = device_id + "/" + signal_id;
if (g_signal_registry && g_signal_registry->is_physics_driven(path)) {
auto val = g_signal_registry->read_signal(path);
if (val) return *val;
}
return internal_state[signal_id];
Thread Safety
Lock policy:
- Physics ticker: Acquire registry mutex ā read actuators ā compute ā write sensors ā release
- Device handlers: Acquire registry mutex ā read values ā release
- No lock held during physics model evaluation (only during registry I/O)
Deadlock prevention: No nested lock acquisition. Physics and device mutexes are separate.
Signal Routing
In mode=sim, routing is declarative in the FluxGraph graph file referenced by
simulation.physics_config (provider config only carries the path).
edges:
- source: device_a/actuator # Read from device
target: model_b/input # Write to model
transform: { type: linear, scale: 100.0 }
- source: model_b/output # Read from model
target: device_c/sensor # Write to device (registry cache)
transform: { type: first_order_lag, tau_s: 2.0 }
FluxGraph processes this schema; provider-sim does not define or validate it.
See FluxGraph docs for authoritative schema details (docs/schema-yaml.md in
the FluxGraph repository).
Device reads from the registry return physics-computed values when the signal is marked physics-driven.
Extension Points
Adding graph transforms/models/rules: update the FluxGraph graph and FluxGraph runtime as needed.
Adding devices: include registry checks in device read_signals()
implementations.
Benefits
- Protocol independence - Physics has zero ADPP dependencies
- Reusability - Same physics engine works with any protocol via
ISignalSource - Declarative config - Full signal flow visible in YAML
- Testability - Mock
ISignalSourcefor unit tests - Debuggability - Registry can log all signal transactions
Design Trade-offs
Cost: Additional indirection layer, mutex overhead on signal access
Benefit: Clean architecture boundaries, long-term maintainability, future-proof for signal recording/replay features