1#[cfg(feature = "with-rerun")]
5use flydra_mvg::FlydraMultiCameraSystem;
6#[cfg(feature = "with-rerun")]
7use num_traits::Float;
8use std::sync::{Arc, RwLock};
9use tracing::{debug, info};
10
11use http_body::Frame;
12use serde::{Deserialize, Serialize};
13
14use event_stream_types::{AcceptsEventStream, EventBroadcaster};
15
16use crate::{Result, TimeDataPassthrough};
17
18use braid_types::{FlydraFloatTimestampLocal, SyncFno, Triggerbox};
19
20const EVENTS_PATH: &str = "/events";
21
22#[cfg(feature = "bundle_files")]
23static ASSETS_DIR: include_dir::Dir<'static> =
24 include_dir::include_dir!("$CARGO_MANIFEST_DIR/static");
25
26async fn events_handler(
27 axum::extract::State(app_state): axum::extract::State<ModelServerAppState>,
28 _: AcceptsEventStream,
29) -> impl axum::response::IntoResponse {
30 let key = {
31 let mut next_connection_id = app_state.next_connection_id.write().unwrap();
32 let key = *next_connection_id;
33 *next_connection_id += 1;
34 key
35 };
36 let (tx, body) = app_state.event_broadcaster.new_connection(key);
37
38 let cal_data = {
40 let current_calibration = app_state.current_calibration.read().unwrap();
42 if let Some((cal_data, tdpt)) = &*current_calibration {
43 let data = (
44 SendType::CalibrationFlydraXml(cal_data.clone()),
45 tdpt.clone(),
46 );
47 Some(data)
48 } else {
49 None
50 }
51 };
52
53 if let Some(cal_data) = cal_data {
55 let cal_body = get_body(&cal_data);
56 tx.send(Frame::data(cal_body.into())).await.unwrap();
57 }
58
59 body
60}
61
62#[derive(Clone)]
63struct ModelServerAppState {
64 current_calibration: Arc<RwLock<Option<(String, TimeDataPassthrough)>>>,
65 event_broadcaster: EventBroadcaster<usize>,
66 next_connection_id: Arc<RwLock<usize>>,
67}
68
69impl Default for ModelServerAppState {
70 fn default() -> Self {
71 Self {
72 current_calibration: Arc::new(RwLock::new(None)),
73 event_broadcaster: Default::default(),
74 next_connection_id: Arc::new(RwLock::new(0)),
75 }
76 }
77}
78
79#[expect(
80 non_snake_case,
81 reason = "fields with covariance are `PXY` to match the Kalman filter covariance matrix layout"
82)]
83#[derive(Debug, Serialize, Deserialize, Clone)]
84pub struct SendKalmanEstimatesRow {
85 pub obj_id: u32,
86 pub frame: SyncFno,
87 pub x: f64,
88 pub y: f64,
89 pub z: f64,
90 pub xvel: f64,
91 pub yvel: f64,
92 pub zvel: f64,
93 pub P00: f64,
94 pub P01: f64,
95 pub P02: f64,
96 pub P11: f64,
97 pub P12: f64,
98 pub P22: f64,
99 pub P33: f64,
100 pub P44: f64,
101 pub P55: f64,
102}
103
104impl From<braid_types::KalmanEstimatesRow> for SendKalmanEstimatesRow {
105 fn from(orig: braid_types::KalmanEstimatesRow) -> SendKalmanEstimatesRow {
106 SendKalmanEstimatesRow {
107 obj_id: orig.obj_id,
108 frame: orig.frame,
109 x: orig.x,
110 y: orig.y,
111 z: orig.z,
112 xvel: orig.xvel,
113 yvel: orig.yvel,
114 zvel: orig.zvel,
115 P00: orig.P00,
116 P01: orig.P01,
117 P02: orig.P02,
118 P11: orig.P11,
119 P12: orig.P12,
120 P22: orig.P22,
121 P33: orig.P33,
122 P44: orig.P44,
123 P55: orig.P55,
124 }
125 }
126}
127
128#[derive(Serialize, Deserialize, Debug, Clone)]
129pub enum SendType {
130 Birth(SendKalmanEstimatesRow),
133 Update(SendKalmanEstimatesRow),
134 Death(u32), EndOfFrame(SyncFno),
137 CalibrationFlydraXml(String),
139}
140
141#[derive(Serialize, Deserialize, Debug)]
142pub struct ToListener {
143 v: u16,
147 msg: SendType,
148 latency: f64,
149 synced_frame: SyncFno,
150 #[serde(with = "braid_types::timestamp_opt_f64")]
151 trigger_timestamp: Option<FlydraFloatTimestampLocal<Triggerbox>>,
152}
153
154pub async fn new_model_server(
155 mut data_rx: tokio::sync::mpsc::Receiver<(SendType, TimeDataPassthrough)>,
156 addr: std::net::SocketAddr,
157) -> Result<()> {
158 let app_state = ModelServerAppState::default();
159
160 let listener = tokio::net::TcpListener::bind(addr).await?;
161
162 #[cfg(feature = "bundle_files")]
163 let serve_dir = tower_serve_static::ServeDir::new(&ASSETS_DIR);
164
165 #[cfg(feature = "serve_files")]
166 let serve_dir = tower_http::services::fs::ServeDir::new(
167 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("static"),
168 );
169
170 let router = axum::Router::new()
172 .route(EVENTS_PATH, axum::routing::get(events_handler))
173 .fallback_service(serve_dir)
174 .with_state(app_state.clone());
175
176 let http_serve_future = {
178 use std::future::IntoFuture;
179 axum::serve(listener, router).into_future()
180 };
181
182 info!("ModelServer at http://{}:{}/", addr.ip(), addr.port());
183
184 debug!(
185 "ModelServer events at http://{}:{}{}",
186 addr.ip(),
187 addr.port(),
188 EVENTS_PATH,
189 );
190
191 let app_state2 = app_state.clone();
193 let new_data_processor_future = async move {
194 let app_state = app_state2;
195
196 const ENV_KEY: &str = "RERUN_VIEWER_URL";
197
198 #[cfg(feature = "with-rerun")]
199 let rec = {
200 let rec = std::env::var_os(ENV_KEY).map(|url_str| {
201 let url = url_str.to_str().unwrap();
202 let re_version = re_sdk::build_info().version;
203 tracing::info!("Streaming data to rerun {re_version} at {url}");
204 re_sdk::RecordingStreamBuilder::new("braid")
205 .connect_grpc_opts(url)
206 .unwrap()
207 });
208
209 if rec.is_none() {
210 tracing::info!(
211 "No Rerun viewer address specified with environment variable \
212 \"{ENV_KEY}\", not logging data to Rerun. (Hint: the Rerun Viewer \
213 listens by default at \"rerun+http://127.0.0.1:9876/proxy\".)"
214 );
215 }
216 rec
217 };
218
219 #[cfg(not(feature = "with-rerun"))]
220 if std::env::var_os(ENV_KEY).is_some() {
221 tracing::warn!(
222 "Rerun support not compiled in, cannot stream to Rerun viewer. \
223 Rebuild with the \"with-rerun\" feature enabled."
224 );
225 }
226
227 #[cfg(feature = "with-rerun")]
228 let mut did_show_rerun_warning = false;
229
230 loop {
232 let opt_new_data = data_rx.recv().await;
233 match &opt_new_data {
234 Some(data) => {
235 if let (SendType::CalibrationFlydraXml(calib), tdpt) = &data {
236 let mut current_calibration =
237 app_state.current_calibration.write().unwrap();
238 *current_calibration = Some((calib.clone(), tdpt.clone()));
239 }
240 send_msg(data, &app_state).await?;
241
242 #[cfg(feature = "with-rerun")]
243 if let Some(rec) = &rec {
244 match data {
245 (SendType::CalibrationFlydraXml(calib_xml), _tdpt) => {
246 let buf = std::io::Cursor::new(calib_xml);
247 let system = FlydraMultiCameraSystem::<f64>::from_flydra_xml(buf)?;
248 for (cam_name, cam) in system.system().cams_by_name().iter() {
249 use braid_mvg::rerun_io::AsRerunTransform3D;
250 const CAMERA_BASE_PATH: &str = "/world/camera";
251 let base_path = format!("{CAMERA_BASE_PATH}/{cam_name}");
252 rec.log(
253 base_path.as_str(),
254 &extrinsics_f64(cam.extrinsics())
255 .as_rerun_transform3d()
256 .into(),
257 )
258 .unwrap();
259 let raw_path = format!("{base_path}/raw");
260 let (w, h) = (cam.width(), cam.height());
261
262 let i = cam.intrinsics();
263 if !i.distortion.is_linear() {
264 if !did_show_rerun_warning {
266 tracing::warn!(
267 "Not showing distortions in rerun. See https://github.com/rerun-io/rerun/issues/2499"
268 );
269 did_show_rerun_warning = true;
270 }
271 }
272 if i.skew().abs() > 1e-15 {
273 tracing::warn!(
274 "Camera has skew, but rerun cameras do not support skew"
275 );
276 }
277 let params = cam_geom::PerspectiveParams {
278 fx: i.fx(),
279 fy: i.fy(),
280 skew: 0.0,
281 cx: i.cx(),
282 cy: i.cy(),
283 };
284 let intrinsics: cam_geom::IntrinsicParametersPerspective<_> =
285 params.into();
286 let pinhole =
288 braid_mvg::rerun_io::cam_geom_to_rr_pinhole_archetype(
289 &intrinsics,
290 w,
291 h,
292 )
293 .unwrap();
294 rec.log(raw_path, &pinhole).unwrap();
295 }
296 }
297 (SendType::Birth(row), _tdpt) | (SendType::Update(row), _tdpt) => {
298 let obj_id = format!("/obj/{}", row.obj_id);
299 let position = re_sdk_types::datatypes::Vec3D::new(
300 row.x as f32,
301 row.y as f32,
302 row.z as f32,
303 );
304 rec.log(
305 obj_id,
306 &re_sdk_types::archetypes::Points3D::new([position]),
307 )
308 .unwrap();
309 }
310 (SendType::Death(obj_id), _tdpt) => {
311 let obj_id = format!("/obj/{obj_id}");
313 let empty_position: [(f32, f32, f32); 0] = [];
314 rec.log(
315 obj_id,
316 &re_sdk_types::archetypes::Points3D::new(empty_position),
317 )
318 .unwrap();
319 }
320 (SendType::EndOfFrame(_x), _tdpt) => {}
321 }
322 }
323 }
324 None => {
325 break;
327 }
328 }
329 }
330 Ok::<_, crate::Error>(())
331 };
332
333 tokio::select! {
335 result = new_data_processor_future => {result?}
336 result = http_serve_future => {result?}
337 }
338 Ok(())
341}
342
343#[cfg(feature = "with-rerun")]
344fn extrinsics_f64<F: nalgebra::RealField + Float>(
346 e: &cam_geom::ExtrinsicParameters<F>,
347) -> cam_geom::ExtrinsicParameters<f64> {
348 let r = e.pose().rotation.as_ref().coords;
349 let rotation: nalgebra::UnitQuaternion<f64> =
350 nalgebra::UnitQuaternion::from_quaternion(nalgebra::Quaternion {
351 coords: nalgebra::Vector4::new(
352 r[0].to_f64().unwrap(),
353 r[1].to_f64().unwrap(),
354 r[2].to_f64().unwrap(),
355 r[3].to_f64().unwrap(),
356 ),
357 });
358 let c = e.camcenter();
359 let camcenter = nalgebra::Point3 {
360 coords: nalgebra::Vector3::new(
361 c[0].to_f64().unwrap(),
362 c[1].to_f64().unwrap(),
363 c[2].to_f64().unwrap(),
364 ),
365 };
366 cam_geom::ExtrinsicParameters::from_rotation_and_camcenter(rotation, camcenter)
367}
368
369fn get_body(data: &(SendType, TimeDataPassthrough)) -> String {
370 let (msg, tdpt) = data;
371 let latency: f64 = if let Some(ref tt) = tdpt.trigger_timestamp() {
372 let now_f64 = strand_datetime_conversion::datetime_to_f64(&chrono::Local::now());
373 now_f64 - tt.as_f64()
374 } else {
375 f64::NAN
376 };
377
378 let data = ToListener {
380 v: 3, msg: msg.clone(),
383 latency,
384 synced_frame: tdpt.synced_frame(),
385 trigger_timestamp: tdpt.trigger_timestamp(),
386 };
387
388 let buf = serde_json::to_string(&data).unwrap();
390 let buf = format!("event: braid\ndata: {buf}\n\n");
392 buf
393}
394
395async fn send_msg(
396 data: &(SendType, TimeDataPassthrough),
397 app_state: &ModelServerAppState,
398) -> Result<()> {
399 let buf = get_body(data);
400 app_state.event_broadcaster.broadcast_frame(buf).await;
401 Ok(())
402}