use std::env;
use std::time::{SystemTime, UNIX_EPOCH};
use isaac_sim_bridge::{LidarFlatScan, LidarPointCloud};
use rerun::{RecordingStream, RecordingStreamBuilder};
use crate::sensor::RerunRender;
const APP_ID: &str = "isaac-sim-rs";
const GRPC_ADDR_ENV: &str = "ISAAC_SIM_RS_RERUN_GRPC_ADDR";
const DEFAULT_GRPC_ADDR: &str = "127.0.0.1:9876";
type BlueprintFn = Box<dyn FnOnce(&RecordingStream) -> eyre::Result<()>>;
type Bind = Box<dyn FnOnce(RecordingStream)>;
#[derive(Default)]
pub struct Viewer {
grpc_addr: Option<String>,
binds: Vec<Bind>,
blueprint: Option<BlueprintFn>,
}
impl Viewer {
pub fn new() -> Self {
Self::default()
}
pub fn with_grpc_addr(mut self, addr: impl Into<String>) -> Self {
self.grpc_addr = Some(addr.into());
self
}
pub fn with_source<S: RerunRender>(
mut self,
_sensor: S,
source: impl Into<String>,
entity_path: impl Into<String>,
) -> Self {
let source = source.into();
let entity_path = entity_path.into();
let label = format!("{}: '{source}' -> '{entity_path}'", S::NAME);
self.binds.push(Box::new(move |rec: RecordingStream| {
log::info!("[isaac-sim-rerun] {label}");
S::register(rec, source, entity_path);
}));
self
}
pub fn with_lidar_flatscan(
self,
source: impl Into<String>,
entity_path: impl Into<String>,
) -> Self {
self.with_source(LidarFlatScan, source, entity_path)
}
pub fn with_lidar_pointcloud(
self,
source: impl Into<String>,
entity_path: impl Into<String>,
) -> Self {
self.with_source(LidarPointCloud, source, entity_path)
}
pub fn with_blueprint<F>(mut self, f: F) -> Self
where
F: FnOnce(&RecordingStream) -> eyre::Result<()> + 'static,
{
self.blueprint = Some(Box::new(f));
self
}
pub fn run(self) -> eyre::Result<()> {
let addr = self.grpc_addr.unwrap_or_else(|| {
env::var(GRPC_ADDR_ENV).unwrap_or_else(|_| DEFAULT_GRPC_ADDR.to_string())
});
let url = format!("rerun+http://{addr}/proxy");
let recording_id = recording_id();
log::info!("[isaac-sim-rerun] connecting to {url} (recording_id={recording_id})");
let mut blueprint = self.blueprint;
for bind in self.binds {
let rec = build_stream(&recording_id, &url)?;
if let Some(bp) = blueprint.take() {
bp(&rec)?;
}
bind(rec);
}
Ok(())
}
}
fn build_stream(recording_id: &str, url: &str) -> eyre::Result<RecordingStream> {
Ok(RecordingStreamBuilder::new(APP_ID)
.recording_id(recording_id)
.connect_grpc_opts(url.to_string())?)
}
fn recording_id() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("isaac-sim-rs-{nanos}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_lidar_subscriptions() {
let v = Viewer::new()
.with_grpc_addr("10.0.0.1:1234")
.with_lidar_flatscan("/A", "a")
.with_lidar_pointcloud("/B", "b")
.with_lidar_pointcloud("/C", "c");
assert_eq!(v.grpc_addr.as_deref(), Some("10.0.0.1:1234"));
assert_eq!(v.binds.len(), 3);
assert!(v.blueprint.is_none());
}
#[test]
fn builder_stores_blueprint_closure() {
let v = Viewer::new().with_blueprint(|_rec| Ok(()));
assert!(v.blueprint.is_some());
}
#[test]
fn recording_id_is_stable_per_call_format() {
let id = recording_id();
assert!(id.starts_with("isaac-sim-rs-"));
assert!(id.len() > "isaac-sim-rs-".len());
}
}