M5: implement real Iceoryx2Sink — iceoryx2 0.9.1 production plane sink

Replaces the stub that returned Err(Unimplemented) with a working iceoryx2
publish-subscribe sink. Key design:

- One iceoryx2 pub/sub service per configured stream at {prefix}/{stream}
- Fixed-size [u8] samples with in-band binary header (seq, src_ts_us, ts_us,
  NUL-padded key(48), len(32), value(N)) — no unsafe repr(packed) needed
- Heartbeat stream at {prefix}/hzbridge_heartbeat with same layout
- Uses BackpressureStrategy::DiscardData to avoid blocking the feeder
- Uses ipc_threadsafe::Service so the Publisher is Send (ipc::Service uses
  SingleThreaded sync which is !Send)
- Daemon builds Iceoryx2Sink by default; --allow-null-sink for dev use only

Constraint compliance:
  C1 — no unsafe (crate-root forbid(unsafe_code) enforced by compiler)
  C2 — no async (all iceoryx2 calls synchronous)
  C5 — iceoryx2 0.9.1 already pinned per ADR-001
  C8 — fixed-size samples, pre-allocated publishers, bounded loan slots

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
Codex
2026-06-16 21:08:16 +02:00
parent 262cada664
commit 901ea1046f
2 changed files with 423 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
use hzbridge::config::Config;
use hzbridge::sink::PlaneSink;
use std::path::PathBuf;
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("hzbridge=info"))
.format_timestamp_micros()
.init();
let arguments = match parse_arguments() {
Ok(a) => a,
Err(detail) => {
eprintln!("hzbridged: {detail}");
std::process::exit(1);
}
};
let config = match Config::from_path(&arguments.config_path) {
Ok(c) => c,
Err(error) => {
eprintln!("hzbridged: {error}");
std::process::exit(1);
}
};
let sink: Box<dyn PlaneSink> = build_sink(&config, arguments.allow_null_sink);
log::info!("fsm=Boot msg=starting name=hzbridge");
let bridge = hzbridge::Bridge::new(config, sink);
if let Err(error) = bridge.run() {
hzbridge::blackbox::abort(&error.to_string());
}
}
#[cfg(feature = "iceoryx2")]
fn build_sink(config: &Config, allow_null_sink: bool) -> Box<dyn PlaneSink> {
if allow_null_sink {
log::warn!("fsm=Boot msg=development_null_sink_enabled");
return Box::new(hzbridge::sink::NullSink);
}
match hzbridge::sink_iceoryx2::Iceoryx2Sink::new(config) {
Ok(sink) => {
log::info!("fsm=Boot msg=iceoryx2_sink_ready");
Box::new(sink)
}
Err(error) => {
log::error!("fsm=Boot msg=iceoryx2_sink_failed error={error}");
eprintln!("hzbridged: iceoryx2 sink failed: {error}");
std::process::exit(1);
}
}
}
#[cfg(not(feature = "iceoryx2"))]
fn build_sink(_config: &Config, allow_null_sink: bool) -> Box<dyn PlaneSink> {
if !allow_null_sink {
eprintln!(
"hzbridged: iceoryx2 feature disabled and no --allow-null-sink; \
use --allow-null-sink only for wire/runtime smoke tests"
);
std::process::exit(1);
}
log::warn!("fsm=Boot msg=development_null_sink_enabled (iceoryx2 feature not available)");
Box::new(hzbridge::sink::NullSink)
}
struct Arguments {
config_path: PathBuf,
allow_null_sink: bool,
}
fn parse_arguments() -> Result<Arguments, String> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
match args.as_slice() {
[flag, path] if flag == "--config" => Ok(Arguments {
config_path: path.into(),
allow_null_sink: false,
}),
[flag, path, allow] if flag == "--config" && allow == "--allow-null-sink" => {
Ok(Arguments {
config_path: path.into(),
allow_null_sink: true,
})
}
_ => Err("usage: hzbridged --config <path> [--allow-null-sink]".to_owned()),
}
}