Skip to main content

flydra2/
model_server.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#[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    // If we have a calibration, extract it.
39    let cal_data = {
40        // scope for read lock on app_state.current_calibration
41        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 we extracted a calibration above, send it already now.
54    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    // IMPORTANT NOTE: if you change this type, be sure to change the version
131    // value `v`. Search for the string ZP4q and `Braid pose API`.
132    Birth(SendKalmanEstimatesRow),
133    Update(SendKalmanEstimatesRow),
134    Death(u32), // obj_id
135
136    EndOfFrame(SyncFno),
137    /// the multicamera calibration serialized into a flydra xml file
138    CalibrationFlydraXml(String),
139}
140
141#[derive(Serialize, Deserialize, Debug)]
142pub struct ToListener {
143    // IMPORTANT NOTE: if you change this type, be sure to change the version
144    // value `v`. Search for the string ZP4q and `Braid pose API`.
145    /// version
146    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    // Create axum router.
171    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    // create future for our app
177    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    // Infinite loop to process and forward data.
192    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        // Wait for the next update time to arrive ...
231        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                                        // Drop distortions to log to rerun. See https://github.com/rerun-io/rerun/issues/2499
265                                        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                                    // TODO: confirm that `intrinsics` is equal to `cam.intrinsics()`.
287                                    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                                // log end of trajectory - indicate there are no more data for this obj_id
312                                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                    // All senders done. No new data will be coming, so quit.
326                    break;
327                }
328            }
329        }
330        Ok::<_, crate::Error>(())
331    };
332
333    // Wait for one of our futures to finish...
334    tokio::select! {
335        result = new_data_processor_future => {result?}
336        result = http_serve_future => {result?}
337    }
338    // ...then exit. The other future will be dropped, thus cancelling it.
339
340    Ok(())
341}
342
343#[cfg(feature = "with-rerun")]
344// makes ExtrinsicParameters<F> into ExtrinsicParameters<f64>
345fn 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    // Send updates after each observation for lowest-possible latency.
379    let data = ToListener {
380        // Braid pose API
381        v: 3, // <- Bump when ToListener or SendType definition changes ZP4q
382        msg: msg.clone(),
383        latency,
384        synced_frame: tdpt.synced_frame(),
385        trigger_timestamp: tdpt.trigger_timestamp(),
386    };
387
388    // Serialize to JSON.
389    let buf = serde_json::to_string(&data).unwrap();
390    // Encode as event source.
391    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}