Skip to main content

braid_types/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Core types for the Braid tracking system.
5//!
6//! This crate provides the fundamental data structures and configuration types
7//! used throughout the Braid multi-camera tracking system, including tracking
8//! parameters, camera configurations, and data storage formats.
9//!
10//! ## Features
11//!
12//! - `with-tokio-codec`: Enables CBOR packet codec for tokio-based applications
13//! - `start-listener`: Enables TCP listener utilities for HTTP servers
14
15#![warn(missing_docs)]
16#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
17
18#[macro_use]
19extern crate static_assertions;
20
21use ordered_float::NotNan;
22use std::net::SocketAddr;
23use strand_cam_bui_types::{ClockModel, RecordingPath};
24
25use serde::{Deserialize, Deserializer, Serialize};
26
27use strand_bui_backend_session_types::BuiServerAddrInfo;
28use strand_withkey::WithKey;
29
30/// Default address for the model server.
31pub const DEFAULT_MODEL_SERVER_ADDR: &str = "0.0.0.0:8397";
32
33// These are the filenames saved during recording. --------------------
34//
35// BraidMetadataSchemaTag (BRAID_SCHEMA) is versioned like semver: it is bumped
36// only on a backward-incompatible change to the on-disk format -- one that
37// would prevent an existing reader from correctly parsing a newly written file.
38// Examples that REQUIRE a bump: removing or renaming a file, removing a CSV
39// column or struct field, or changing the type or meaning of an existing one.
40// Purely additive changes -- a new file, a new optional CSV column, or a new
41// `#[serde(default)]` struct field -- are backward compatible and do NOT bump
42// the schema, since old readers continue to work unchanged.
43/// Version number for the Braid metadata schema.
44pub const BRAID_SCHEMA: u16 = 3; // BraidMetadataSchemaTag
45
46// CSV files. (These may also exist as .csv.gz)
47/// CSV filename for Kalman filter estimates.
48pub const KALMAN_ESTIMATES_CSV_FNAME: &str = "kalman_estimates.csv";
49/// CSV filename for data association records.
50pub const DATA_ASSOCIATE_CSV_FNAME: &str = "data_association.csv";
51/// CSV filename for 2D distorted coordinate data.
52pub const DATA2D_DISTORTED_CSV_FNAME: &str = "data2d_distorted.csv";
53/// CSV filename for camera information.
54pub const CAM_INFO_CSV_FNAME: &str = "cam_info.csv";
55/// CSV filename for trigger clock information.
56pub const TRIGGER_CLOCK_INFO_CSV_FNAME: &str = "trigger_clock_info.csv";
57/// CSV filename for experiment information.
58pub const EXPERIMENT_INFO_CSV_FNAME: &str = "experiment_info.csv";
59/// CSV filename for text log messages.
60pub const TEXTLOG_CSV_FNAME: &str = "textlog.csv";
61
62// Other files
63/// XML filename for camera calibration data.
64pub const CALIBRATION_XML_FNAME: &str = "calibration.xml";
65/// YAML filename for Braid metadata.
66pub const BRAID_METADATA_YML_FNAME: &str = "braid_metadata.yml";
67/// Markdown filename for README documentation.
68pub const README_MD_FNAME: &str = "README.md";
69/// Directory name for saved images.
70pub const IMAGES_DIRNAME: &str = "images";
71/// Directory name for camera settings.
72pub const CAM_SETTINGS_DIRNAME: &str = "cam_settings";
73/// Directory name for feature detection settings.
74pub const FEATURE_DETECT_SETTINGS_DIRNAME: &str = "feature_detect_settings";
75/// HLog filename for reconstruction latency measurements.
76pub const RECONSTRUCT_LATENCY_HLOG_FNAME: &str = "reconstruct_latency_usec.hlog";
77/// HLog filename for reprojection distance measurements.
78pub const REPROJECTION_DIST_HLOG_FNAME: &str = "reprojection_distance_100x_pixels.hlog";
79
80/// Duration in seconds for triggerbox synchronization.
81pub const TRIGGERBOX_SYNC_SECONDS: u64 = 3;
82
83// Ideas for future:
84//
85// Make tracking model and parameters "pluggable" so that other models - with
86// different structure - can be easily used.
87//
88// **statistics cache for data2d_distorted** We could keep a statistics cache as
89// we write a braidz file for things like num found points, average and maximum
90// values etc. This could be periodically flushed to disk and recomputed anytime
91// but would eliminate most needs to iterate over the entire dataset at read
92// time.
93//
94// **statistics cache for kalman_estimates** Same as above but 3D.
95//
96// Cache the camera pixel sizes. Currently this can be found if images are saved
97// or if the a camera calibration is present. The images in theory are always
98// there but this is not currently implemented in the strand-cam "flydratrax"
99// mode. Even when that is fixed, to simply read the image size that way will
100// require parsing an entire image parser.
101//
102// Replace `TrackingParams.initial_position_std_meters` and
103// `TrackingParams.initial_vel_std_meters_per_sec` with a scaled version of the
104// process covariance matrix Q. According to this ([p.
105// 18](https://www.robots.ox.ac.uk/~ian/Teaching/Estimation/LectureNotes2.pdf)),
106// this approach is common with a scale factor of 10.
107// --------------------------------------------------------------------
108
109/// Camera information record for CSV output.
110// Backward-incompatible changes here require a BRAID_SCHEMA bump (see its definition).
111#[derive(Debug, Serialize, Deserialize, Clone)]
112pub struct CamInfoRow {
113    /// The index of the camera. This changes from invocation to invocation of Braid.
114    pub camn: CamNum,
115    /// The name of the camera. This is stable across invocations of Braid.
116    ///
117    /// Any valid UTF-8 string is possible. (Previously, this was the "ROS name"
118    /// of the camera in which, e.g. '-' was replaced with '_'. This is no
119    /// longer the case.)
120    pub cam_id: String,
121}
122
123/// Kalman filter state estimate record for CSV output.
124// Backward-incompatible changes here require a BRAID_SCHEMA bump (see its definition).
125#[expect(
126    non_snake_case,
127    reason = "fields with covariance are named after the standard Kalman filter covariance matrix notation."
128)]
129#[derive(Debug, Serialize, Deserialize, Clone)]
130pub struct KalmanEstimatesRow {
131    /// Object ID being tracked.
132    pub obj_id: u32,
133    /// Synchronized frame number.
134    pub frame: SyncFno,
135    /// The timestamp when the trigger pulse fired.
136    ///
137    /// Note that calculating this live in braid requires that the clock model
138    /// has established itself. Thus, the initial frames immediately after
139    /// synchronization will not have a timestamp.
140    #[serde(with = "crate::timestamp_opt_f64")]
141    pub timestamp: Option<FlydraFloatTimestampLocal<Triggerbox>>,
142    /// X position estimate in meters.
143    pub x: f64,
144    /// Y position estimate in meters.
145    pub y: f64,
146    /// Z position estimate in meters.
147    pub z: f64,
148    /// X velocity estimate in meters per second.
149    pub xvel: f64,
150    /// Y velocity estimate in meters per second.
151    pub yvel: f64,
152    /// Z velocity estimate in meters per second.
153    pub zvel: f64,
154    /// Covariance matrix element P\[0,0\].
155    pub P00: f64,
156    /// Covariance matrix element P\[0,1\].
157    pub P01: f64,
158    /// Covariance matrix element P\[0,2\].
159    pub P02: f64,
160    /// Covariance matrix element P\[1,1\].
161    pub P11: f64,
162    /// Covariance matrix element P\[1,2\].
163    pub P12: f64,
164    /// Covariance matrix element P\[2,2\].
165    pub P22: f64,
166    /// Covariance matrix element P\[3,3\].
167    pub P33: f64,
168    /// Covariance matrix element P\[4,4\].
169    pub P44: f64,
170    /// Covariance matrix element P\[5,5\].
171    pub P55: f64,
172}
173impl WithKey<SyncFno> for KalmanEstimatesRow {
174    fn key(&self) -> SyncFno {
175        self.frame
176    }
177}
178
179/// Data association record linking 2D detections to 3D tracks.
180#[derive(Debug, Serialize, Deserialize, Clone)]
181pub struct DataAssocRow {
182    // backward-incompatible changes here require a BRAID_SCHEMA bump (see its definition)
183    /// Object ID being tracked.
184    pub obj_id: u32,
185    /// Synchronized frame number.
186    pub frame: SyncFno,
187    /// Camera number.
188    pub cam_num: CamNum,
189    /// Point index within the frame.
190    pub pt_idx: u8,
191}
192impl WithKey<SyncFno> for DataAssocRow {
193    fn key(&self) -> SyncFno {
194        self.frame
195    }
196}
197
198/// A 2D feature detection result transmitted via UDP.
199#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
200pub struct FlydraRawUdpPoint {
201    /// X coordinate in absolute pixels.
202    pub x0_abs: f64,
203    /// Y coordinate in absolute pixels.
204    pub y0_abs: f64,
205    /// Area of the detection in pixels.
206    pub area: f64,
207    /// Optional slope and eccentricity values.
208    pub maybe_slope_eccentricty: Option<(f64, f64)>,
209    /// Current pixel value.
210    pub cur_val: u8,
211    /// Mean pixel value.
212    pub mean_val: f64,
213    /// Sum of squares of pixel values.
214    pub sumsqf_val: f64,
215}
216
217/// The original camera name from the driver.
218#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, PartialOrd, Ord)]
219pub struct RawCamName(String);
220
221impl RawCamName {
222    /// Create a new RawCamName from a string.
223    pub fn new(s: String) -> Self {
224        RawCamName(s)
225    }
226    /// Get the camera name as a string slice.
227    pub fn as_str(&self) -> &str {
228        &self.0
229    }
230}
231
232impl std::fmt::Display for RawCamName {
233    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
234        write!(f, "{}", self.0)
235    }
236}
237
238/// HTTP API utilities for Braid server.
239pub mod braid_http {
240    /// URL path for remote camera info endpoint.
241    pub const REMOTE_CAMERA_INFO_PATH: &str = "remote-camera-info";
242    /// URL path for camera proxy endpoint.
243    pub const CAM_PROXY_PATH: &str = "cam-proxy";
244
245    /// Encode camera name, potentially with slashes or spaces, to be a single
246    /// URL path component.
247    ///
248    /// Use percent-encoding, which `axum::extract::Path` automatically decodes.
249    pub fn encode_cam_name(cam_name: &crate::RawCamName) -> String {
250        percent_encoding::utf8_percent_encode(&cam_name.0, percent_encoding::NON_ALPHANUMERIC)
251            .to_string()
252    }
253}
254
255/// Frame rate limiting configuration for camera startup.
256#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
257pub enum StartSoftwareFrameRateLimit {
258    /// Set the frame_rate limit at a given frame rate.
259    Enable(f64),
260    /// Disable the frame_rate limit.
261    Disabled,
262    /// Do not change the frame rate limit.
263    #[default]
264    NoChange,
265}
266
267/// Camera startup information sent to strand cameras.
268#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
269pub struct RemoteCameraInfoResponse {
270    /// The destination UDP port to use for low-latency tracking data
271    pub camdata_udp_port: u16,
272    /// Camera configuration.
273    pub config: BraidCameraConfig,
274    /// Whether to force camera synchronization mode.
275    pub force_camera_sync_mode: bool,
276    /// Software frame rate limiting configuration.
277    pub software_limit_framerate: StartSoftwareFrameRateLimit,
278    /// camera triggering configuration (global for all cameras)
279    pub trig_config: TriggerType,
280}
281
282/// Newtype storing time as number of nanoseconds since Jan 1, 1970 in UTC.
283///
284/// This is the lower 64 bits of the 80 bit PTP timestamp.
285#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
286pub struct PtpStamp(u64);
287
288impl PtpStamp {
289    /// Create a new PtpStamp from nanoseconds since epoch.
290    pub fn new(val: u64) -> Self {
291        PtpStamp(val)
292    }
293
294    /// Get the raw nanoseconds value.
295    pub fn get(&self) -> u64 {
296        self.0
297    }
298
299    /// Calculate duration since another timestamp.
300    pub fn duration_since(&self, other: &Self) -> Option<PtpStampDuration> {
301        if self.0 >= other.0 {
302            Some(PtpStampDuration(self.0 - other.0))
303        } else {
304            None
305        }
306    }
307}
308
309/// Newtype storing a duration between two [PtpStamp] values.
310#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
311pub struct PtpStampDuration(u64);
312
313impl PtpStampDuration {
314    /// Get the duration in nanoseconds.
315    pub fn nanos(&self) -> u64 {
316        self.0
317    }
318}
319
320impl<TZ> TryFrom<chrono::DateTime<TZ>> for PtpStamp
321where
322    TZ: chrono::TimeZone,
323{
324    type Error = &'static str;
325
326    fn try_from(orig: chrono::DateTime<TZ>) -> Result<Self, Self::Error> {
327        Ok(Self(
328            orig.to_utc()
329                .timestamp_nanos_opt()
330                .ok_or("could not convert DateTime to i64 nanosec")?
331                .try_into()
332                .map_err(|_| "could not convert i64 nanosec to u64")?,
333        ))
334    }
335}
336
337impl TryFrom<PtpStamp> for chrono::DateTime<chrono::Utc> {
338    type Error = &'static str;
339    fn try_from(orig: PtpStamp) -> Result<Self, Self::Error> {
340        let secs = orig.0 / 1_000_000_000;
341        let nsecs = orig.0 % 1_000_000_000;
342        chrono::DateTime::from_timestamp(
343            secs.try_into()
344                .map_err(|_| "could not convert u64 nanosec to i64")?,
345            nsecs
346                .try_into()
347                .map_err(|_| "could not convert u64 nanosec to u32")?,
348        )
349        .ok_or("could not convert timestamp to DateTime")
350    }
351}
352
353impl TryFrom<PtpStamp> for chrono::DateTime<chrono::FixedOffset> {
354    type Error = &'static str;
355    fn try_from(orig: PtpStamp) -> Result<Self, Self::Error> {
356        let utc: chrono::DateTime<chrono::Utc> = orig.try_into()?;
357        Ok(utc.into())
358    }
359}
360
361impl TryFrom<PtpStamp> for chrono::DateTime<chrono::Local> {
362    type Error = &'static str;
363    fn try_from(orig: PtpStamp) -> Result<Self, Self::Error> {
364        let utc: chrono::DateTime<chrono::Utc> = orig.try_into()?;
365        Ok(utc.into())
366    }
367}
368
369/// Default allowed imprecision for frame acquisition duration in milliseconds.
370pub const DEFAULT_ACQUISITION_DURATION_ALLOWED_IMPRECISION_MSEC: Option<f64> = Some(5.0);
371
372/// Configuration for a single camera in the Braid system.
373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
374#[serde(deny_unknown_fields)]
375pub struct BraidCameraConfig {
376    /// The name of the camera (e.g. "Basler-22005677")
377    ///
378    /// (This is the original UTF-8 camera name, not the ROS-encoded camera name
379    /// in which certain characters are not allowed.)
380    pub name: String,
381    /// Filename of vendor-specific camera settings file.
382    ///
383    /// Can contain shell variables such as `~`, `$A`, or `${B}`.
384    pub camera_settings_filename: Option<std::path::PathBuf>,
385    /// The pixel format to use.
386    pub pixel_format: Option<String>,
387    /// Configuration for detecting points.
388    #[serde(default = "flydra_pt_detect_cfg::default_absdiff")]
389    pub point_detection_config: flydra_feature_detector_types::ImPtDetectCfg,
390    /// Which camera backend to use.
391    #[serde(default)]
392    pub start_backend: StartCameraBackend,
393    /// Allowed imprecision for frame acquisition duration.
394    pub acquisition_duration_allowed_imprecision_msec: Option<f64>,
395    /// The SocketAddr on which the strand camera BUI server should run.
396    pub http_server_addr: Option<String>,
397    /// The interval at which the current image should be sent, in milliseconds.
398    #[serde(default = "default_send_current_image_interval_msec")]
399    pub send_current_image_interval_msec: u64,
400
401    /// Deprecated, useless old config option (not removed for backwards compatibility)
402    #[serde(
403        default,
404        skip_serializing,
405        rename = "raise_grab_thread_priority",
406        deserialize_with = "raise_grab_thread_priority_deser"
407    )]
408    _raise_grab_thread_priority: bool,
409}
410
411fn raise_grab_thread_priority_deser<'de, D>(de: D) -> Result<bool, D::Error>
412where
413    D: Deserializer<'de>,
414{
415    tracing::error!(
416        "The parameter 'raise_grab_thread_priority' is no longer used. Remove this parameter from your configuration."
417    );
418    bool::deserialize(de)
419}
420
421const fn default_send_current_image_interval_msec() -> u64 {
422    2000
423}
424
425/// Camera backend selection for local camera startup.
426#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
427#[serde(rename_all = "lowercase")]
428#[derive(Default)]
429pub enum StartCameraBackend {
430    /// Do not start a camera locally. Rather, wait for a remote camera to connect.
431    Remote,
432    /// Start a Pylon camera locally using the `strand-cam` program with the
433    /// `--camera-backend pylon` argument.
434    #[default]
435    Pylon,
436    /// Start a Vimba camera locally using the `strand-cam` program with the
437    /// `--camera-backend vimba` argument.
438    Vimba,
439    /// Start a consumer webcam locally using the `strand-cam` program with
440    /// the `--camera-backend webcam` argument.
441    ///
442    /// Intended for development use. The camera `name` to use in the
443    /// configuration is the human-readable device name of the webcam.
444    Webcam,
445    /// Start a simulated camera locally using the `strand-cam` program with the
446    /// `--camera-backend sim` argument.
447    ///
448    /// Used for end-to-end testing with no hardware: the camera renders
449    /// synthetic images of simulated insects. The scenario is given by the
450    /// `STRAND_CAM_SIM_SPEC` environment variable, and the camera `name` is one
451    /// of the simulated camera names (e.g. `simcam0`).
452    Sim,
453}
454
455impl StartCameraBackend {
456    /// Get the executable name for the camera backend.
457    ///
458    /// All local backends are served by the single merged `strand-cam`
459    /// executable, which selects the backend at runtime via
460    /// [Self::camera_backend_arg].
461    pub fn strand_cam_exe_name(&self) -> Option<&str> {
462        match self {
463            StartCameraBackend::Remote => None,
464            StartCameraBackend::Pylon
465            | StartCameraBackend::Vimba
466            | StartCameraBackend::Webcam
467            | StartCameraBackend::Sim => Some("strand-cam"),
468        }
469    }
470
471    /// Get the value for the `--camera-backend` argument of the `strand-cam`
472    /// executable, or `None` for a remote camera (whose backend is chosen on
473    /// the remote machine).
474    pub fn camera_backend_arg(&self) -> Option<&'static str> {
475        match self {
476            StartCameraBackend::Remote => None,
477            StartCameraBackend::Pylon => Some("pylon"),
478            StartCameraBackend::Vimba => Some("vimba"),
479            StartCameraBackend::Webcam => Some("webcam"),
480            StartCameraBackend::Sim => Some("sim"),
481        }
482    }
483}
484
485impl BraidCameraConfig {
486    /// Create a default camera configuration with absolute difference point detection.
487    pub fn default_absdiff_config(name: String) -> Self {
488        Self {
489            name,
490            camera_settings_filename: None,
491            pixel_format: None,
492            point_detection_config: flydra_pt_detect_cfg::default_absdiff(),
493            _raise_grab_thread_priority: Default::default(),
494            start_backend: Default::default(),
495            acquisition_duration_allowed_imprecision_msec:
496                DEFAULT_ACQUISITION_DURATION_ALLOWED_IMPRECISION_MSEC,
497            http_server_addr: None,
498            send_current_image_interval_msec: default_send_current_image_interval_msec(),
499        }
500    }
501}
502
503/// Per-camera data to be saved during recording.
504#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
505pub struct PerCamSaveData {
506    /// Current image as PNG data.
507    pub current_image_png: PngImageData,
508    /// Current camera settings data.
509    pub cam_settings_data: Option<UpdateCamSettings>,
510    /// Current feature detection settings.
511    pub feature_detect_settings: Option<UpdateFeatureDetectSettings>,
512}
513
514/// Camera registration message sent to Braid.
515#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
516pub struct RegisterNewCamera {
517    /// The name of the camera as returned by the camera
518    pub raw_cam_name: RawCamName,
519    /// Location of the camera control HTTP server.
520    pub http_camserver_info: Option<BuiServerInfo>,
521    /// The camera settings.
522    pub cam_settings_data: Option<UpdateCamSettings>,
523    /// The current image.
524    pub current_image_png: PngImageData,
525    /// The period of the periodic signal generator in the camera.
526    /// This is used for PTP-based synchronization.
527    pub camera_periodic_signal_period_usec: Option<f64>,
528}
529
530/// Image update message.
531#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
532pub struct UpdateImage {
533    /// The current image.
534    pub current_image_png: PngImageData,
535}
536
537/// PNG image data container.
538#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
539pub struct PngImageData {
540    /// Raw PNG image data bytes.
541    pub data: Vec<u8>,
542}
543
544impl From<Vec<u8>> for PngImageData {
545    fn from(data: Vec<u8>) -> Self {
546        Self { data }
547    }
548}
549
550impl PngImageData {
551    /// Get the PNG data as a byte slice.
552    pub fn as_slice(&self) -> &[u8] {
553        self.data.as_slice()
554    }
555
556    /// Get the image dimensions as (width, height) in pixels.
557    ///
558    /// Returns `None` if the data is not valid PNG data.
559    pub fn dimensions(&self) -> Option<(u32, u32)> {
560        // The PNG format starts with an 8 byte signature followed by the IHDR
561        // chunk, which the specification requires to be first. A chunk starts
562        // with a 4 byte length and 4 byte type, after which the IHDR data
563        // begins with the big-endian u32 width and height.
564        const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
565        let data = self.data.as_slice();
566        if data.len() < 24 || data[0..8] != SIGNATURE || &data[12..16] != b"IHDR" {
567            return None;
568        }
569        let width = u32::from_be_bytes(data[16..20].try_into().unwrap());
570        let height = u32::from_be_bytes(data[20..24].try_into().unwrap());
571        Some((width, height))
572    }
573}
574
575impl std::fmt::Debug for PngImageData {
576    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577        write!(f, "PngImageData{{..}}",)
578    }
579}
580
581#[test]
582fn test_png_dimensions() {
583    // Minimal PNG header: signature, IHDR chunk length and type, width 640,
584    // height 480.
585    let mut data = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
586    data.extend_from_slice(&13u32.to_be_bytes());
587    data.extend_from_slice(b"IHDR");
588    data.extend_from_slice(&640u32.to_be_bytes());
589    data.extend_from_slice(&480u32.to_be_bytes());
590    let png = PngImageData::from(data);
591    assert_eq!(png.dimensions(), Some((640, 480)));
592
593    assert_eq!(PngImageData::from(vec![0u8; 24]).dimensions(), None);
594    assert_eq!(PngImageData::from(vec![]).dimensions(), None);
595}
596
597/// Camera settings update message.
598#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
599pub struct UpdateCamSettings {
600    /// The current camera settings
601    pub current_cam_settings_buf: String,
602    /// The filename extension for the camera settings
603    pub current_cam_settings_extension: String,
604}
605
606/// Feature detection settings update message.
607#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
608pub struct UpdateFeatureDetectSettings {
609    /// The current feature detection settings.
610    pub current_feature_detect_settings: flydra_feature_detector_types::ImPtDetectCfg,
611}
612
613/// Camera synchronization state.
614#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
615pub enum ConnectedCameraSyncState {
616    /// No known reference to other cameras
617    Unsynchronized,
618    /// This `u64` is frame0, the offset to go from camera frame to sync frame.
619    Synchronized(u64),
620}
621
622impl ConnectedCameraSyncState {
623    /// Check if the camera is synchronized.
624    pub fn is_synchronized(&self) -> bool {
625        match self {
626            ConnectedCameraSyncState::Unsynchronized => false,
627            ConnectedCameraSyncState::Synchronized(_) => true,
628        }
629    }
630}
631
632/// Information about a newer available release of Braid.
633///
634/// Populated by the background version check when the version-check server
635/// reports a version newer than the running one. Surfaced to every connected
636/// browser as a dismissible banner.
637#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
638pub struct VersionUpdate {
639    /// The newest available version as a semver string, e.g. `"1.0.0-rc.3"`.
640    pub available: String,
641    /// Human-readable message from the version-check server.
642    pub message: String,
643    /// URL with release notes / downloads, rendered as a link.
644    pub url: String,
645}
646
647/// Shared state for the Braid HTTP API.
648#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
649pub struct BraidHttpApiSharedState {
650    /// Camera synchronization trigger configuration.
651    pub trigger_type: TriggerType,
652    /// Whether a clock model is needed for synchronization.
653    pub needs_clock_model: bool,
654    /// The current clock model for time synchronization.
655    pub clock_model: Option<ClockModel>,
656    /// Directory path for CSV table recordings.
657    pub csv_tables_dirname: Option<RecordingPath>,
658    // This is "fake" because it only signals if each of the connected computers
659    // is recording MKVs.
660    /// Path for MP4 recording (signals if cameras are recording).
661    pub fake_mp4_recording_path: Option<RecordingPath>,
662    /// Size of post-trigger buffer in frames.
663    pub post_trigger_buffer_size: usize,
664    /// Filename of camera calibration file.
665    pub calibration_filename: Option<String>,
666    /// List of connected camera information.
667    pub connected_cameras: Vec<CamInfo>, // TODO: make this a BTreeMap?
668    /// Whether the feature detection background model is continuously
669    /// updating, per camera.
670    ///
671    /// Cameras whose feature detection settings have not (yet) been received
672    /// are absent from this map.
673    pub background_model_updating: std::collections::BTreeMap<RawCamName, bool>,
674    /// Image dimensions as (width, height) in pixels, per camera.
675    ///
676    /// Cameras whose image has not (yet) been received are absent from this
677    /// map.
678    pub camera_image_dimensions: std::collections::BTreeMap<RawCamName, (u32, u32)>,
679    /// Address of the model server.
680    pub model_server_addr: Option<SocketAddr>,
681    /// Name of the Flydra application.
682    pub flydra_app_name: String,
683    /// Whether all expected cameras are synchronized.
684    pub all_expected_cameras_are_synced: bool,
685    /// A newer available release discovered by the background version check, or
686    /// `None` if up to date (or the check has not yet found a newer version).
687    pub version_update: Option<VersionUpdate>,
688}
689
690/// Statistics for recent camera activity.
691#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
692pub struct RecentStats {
693    /// Total number of frames collected since start.
694    pub total_frames_collected: usize,
695    /// Number of frames collected in recent period.
696    pub frames_collected: usize,
697    /// Measured frame rate (frames per second) over the recent period.
698    ///
699    /// Computed on the mainbrain from the exact elapsed time of the
700    /// measurement window, so it is stable regardless of how the shared state
701    /// is delivered to clients.
702    pub fps: f64,
703    /// Number of points detected in recent period.
704    pub points_detected: usize,
705}
706
707/// Generic HTTP API server information
708///
709/// This is used for both the Strand Camera BUI and the Braid BUI.
710#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
711pub enum BuiServerInfo {
712    /// No server is present (e.g. prerecorded data).
713    NoServer,
714    /// A server is available.
715    Server(BuiServerAddrInfo),
716}
717
718/// Check if a URL refers to a loopback address.
719pub fn is_loopback(url: &http::Uri) -> bool {
720    let authority = match url.authority() {
721        None => return false,
722        Some(authority) => authority,
723    };
724    match authority.host() {
725        "127.0.0.1" | "[::1]" => true,
726        // should we include "localhost"? only if it actually resolves?
727        _ => false,
728    }
729}
730
731// -----
732
733/// Duration for which a freshly minted access token remains valid.
734///
735/// A token is only needed for a client's very first request: a successful auth
736/// hands back a session cookie that carries the session from then on (browsers
737/// persist it, and Braid persists its per-camera cookie jar to disk). Keeping
738/// the token short-lived bounds the window in which a token leaked via a URL
739/// (terminal scrollback, log files, a photographed QR code) can be replayed.
740#[cfg(feature = "start-listener")]
741pub const ACCESS_TOKEN_TTL: std::time::Duration = std::time::Duration::from_secs(30 * 60);
742
743/// Start a TCP listener for an HTTP server, minting an access token if the
744/// listen address is not loopback.
745///
746/// `persistent_secret` is the same cookie/MAC key used to build the server's
747/// auth layer. The minted token is a self-expiring value signed with that
748/// secret, so the auth layer accepts it without any token value being stored;
749/// see [ACCESS_TOKEN_TTL] for its lifetime.
750#[cfg(feature = "start-listener")]
751pub async fn start_listener(
752    address_string: &str,
753    persistent_secret: &cookie::Key,
754) -> eyre::Result<(tokio::net::TcpListener, BuiServerAddrInfo)> {
755    let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&address_string)?
756        .next()
757        .ok_or_else(|| eyre::eyre!("no address found for HTTP server"))?;
758
759    let listener = tokio::net::TcpListener::bind(socket_addr).await?;
760    let listener_local_addr = listener.local_addr()?;
761    let token = if !listener_local_addr.ip().is_loopback() {
762        // Mint a short-lived, self-expiring token signed with the persistent
763        // secret. The auth layer is configured with the same secret, so it
764        // validates this token by signature and expiry; nothing is stored.
765        let token_str = axum_token_auth::generate_token(persistent_secret, ACCESS_TOKEN_TTL);
766        strand_bui_backend_session_types::AccessToken::PreSharedToken(token_str)
767    } else {
768        strand_bui_backend_session_types::AccessToken::NoToken
769    };
770    let http_camserver_info = BuiServerAddrInfo::new(listener_local_addr, token);
771
772    Ok((listener, http_camserver_info))
773}
774
775/// Parse a list of CIDR strings (e.g. `"100.64.0.0/10"`) into the network type
776/// expected by [`axum_token_auth::AuthConfig::trusted_networks`], returning a
777/// descriptive error for the first one that fails to parse.
778#[cfg(feature = "start-listener")]
779pub fn parse_trusted_networks(nets: &[String]) -> eyre::Result<Vec<axum_token_auth::CidrBlock>> {
780    nets.iter()
781        .map(|s| {
782            s.parse::<axum_token_auth::CidrBlock>()
783                .map_err(|e| eyre::eyre!("invalid trusted network CIDR {s:?}: {e}"))
784        })
785        .collect()
786}
787
788/// Restrict the on-disk `preferences_serde1` file backing `key` to owner-only
789/// access (Unix mode 0600), warning if it was previously reachable by other
790/// local users.
791///
792/// The cookie/token secret is effectively a master credential (it can forge any
793/// session and mint any token) and the persisted cookie jars hold live session
794/// cookies, so neither should be group- or world-readable. `preferences_serde1`
795/// creates the file with the process umask (typically 0644); this tightens it
796/// after the fact. No-op on non-Unix platforms, whose permission model differs.
797#[cfg(feature = "start-listener")]
798pub fn harden_prefs_file(app: &preferences_serde1::AppInfo, key: &str) {
799    #[cfg(unix)]
800    {
801        use std::os::unix::fs::PermissionsExt;
802
803        // Mirror `preferences_serde1`'s path layout: <config>/<app>/<key>.prefs.json
804        let Some(mut path) = preferences_serde1::prefs_base_dir() else {
805            return;
806        };
807        path.push(app.name);
808        path.push(key);
809        let Some(mut name) = path.file_name().map(|n| n.to_os_string()) else {
810            return;
811        };
812        name.push(".prefs.json");
813        path.set_file_name(name);
814
815        let Ok(md) = std::fs::metadata(&path) else {
816            // No file on disk (e.g. secret supplied via override): nothing to do.
817            return;
818        };
819        let mode = md.permissions().mode() & 0o777;
820        if mode & 0o077 != 0 {
821            tracing::warn!(
822                "Restricting permissions on sensitive file {} from {mode:o} to 600 \
823                 (it was accessible to other local users).",
824                path.display(),
825            );
826        }
827        if let Err(e) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) {
828            tracing::warn!("Could not restrict permissions on {}: {e}", path.display());
829        }
830    }
831    #[cfg(not(unix))]
832    {
833        let _ = (app, key);
834    }
835}
836
837// -----
838
839/// Text log message record for CSV output.
840#[derive(Debug, Serialize, Deserialize)]
841pub struct TextlogRow {
842    // backward-incompatible changes here require a BRAID_SCHEMA bump (see its definition)
843    /// Timestamp from the main brain system.
844    pub mainbrain_timestamp: f64,
845    /// Camera identifier.
846    pub cam_id: String,
847    /// Host system timestamp.
848    pub host_timestamp: f64,
849    /// Log message text.
850    pub message: String,
851}
852
853/// Tracking parameters
854///
855/// The terminology used is as defined at [the Wikipedia page on the Kalman
856/// filter](https://en.wikipedia.org/wiki/Kalman_filter).
857///
858/// The state estimated is a six component vector with position and velocity
859/// **x** = \<x, y, z, x', y', z'\>. The motion model is a constant velocity
860/// model with noise term, (see
861/// [description](https://webee.technion.ac.il/people/shimkin/Estimation09/ch8_target.pdf)).
862///
863/// The state covariance matrix **P** is initialized with the value (α is
864/// defined in the field [TrackingParams::initial_position_std_meters] and β is
865/// defined in the field [TrackingParams::initial_vel_std_meters_per_sec]:<br/>
866/// **P**<sub>initial</sub> = [[α<sup>2</sup>, 0, 0, 0, 0, 0],<br/>
867/// [0, α<sup>2</sup>, 0, 0, 0, 0],<br/>
868/// [0, 0, α<sup>2</sup>, 0, 0, 0],<br/>
869/// [0, 0, 0, β<sup>2</sup>, 0, 0],<br/>
870/// [0, 0, 0, 0, β<sup>2</sup>, 0],<br/>
871/// [0, 0, 0, 0, 0, β<sup>2</sup>]]
872///
873/// The covariance of the state process update **Q**(τ) is defined as a function
874/// of τ, the time interval from the previous update):<br/>
875/// **Q**(τ) = [TrackingParams::motion_noise_scale] [[τ<sup>3</sup>/3, 0, 0, τ<sup>2</sup>/2, 0,
876/// 0],<br/>
877/// [0, τ<sup>3</sup>/3, 0, 0, τ<sup>2</sup>/2, 0],<br/>
878/// [0, 0, τ<sup>3</sup>/3, 0, 0, τ<sup>2</sup>/2],<br/>
879/// [τ<sup>2</sup>/2, 0, 0, τ, 0, 0],<br/>
880/// [0, τ<sup>2</sup>/2, 0, 0, τ, 0],<br/>
881/// [0, 0, τ<sup>2</sup>/2, 0, 0, τ]]
882///
883/// Note that this form of the state process update covariance has the property
884/// that 2**Q**(τ) = **Q**(2τ). In other words, two successive additions of this
885/// covariance will have an identical effect to a single addtion for twice the
886/// time interval.
887#[derive(Debug, Clone, Serialize, Deserialize)]
888#[serde(deny_unknown_fields)]
889pub struct TrackingParams {
890    /// This is used to scale the state noise covariance matrix **Q** as
891    /// described at the struct-level (Kalman filter parameter).
892    pub motion_noise_scale: f64,
893    /// This is α in the above formula used to build the position terms in the
894    /// initial estimate covariance matrix **P** as described at the
895    /// struct-level (Kalman filter parameter).
896    pub initial_position_std_meters: f64,
897    /// This is β in the above formula used to build the velocity terms in the
898    /// initial estimate covariance matrix **P** as described at the
899    /// struct-level (Kalman filter parameter).
900    pub initial_vel_std_meters_per_sec: f64,
901    /// The observation noise covariance matrix **R** (Kalman filter
902    /// parameter).
903    pub ekf_observation_covariance_pixels: f64,
904    /// This sets a minimum threshold for using an obervation to update an
905    /// object being tracked (data association parameter).
906    pub accept_observation_min_likelihood: f64,
907    /// This is used to compute the maximum allowable covariance before an
908    /// object is "killed" and no longer tracked.
909    pub max_position_std_meters: f32,
910    /// These are the hypothesis testing parameters used to "birth" a new new
911    /// object and start tracking it.
912    ///
913    /// This is `None` if 2D (flat-3d) tracking.
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub hypothesis_test_params: Option<HypothesisTestParams>,
916    /// This is the minimum number of observations before object becomes
917    /// visible.
918    #[serde(default = "default_num_observations_to_visibility")]
919    pub num_observations_to_visibility: u8,
920    /// Parameters defining mini arena configuration.
921    ///
922    /// This is MiniArenaConfig::NoMiniArena if no mini arena is in use.
923    #[serde(skip_serializing_if = "MiniArenaConfig::is_none", default)]
924    pub mini_arena_config: MiniArenaConfig,
925}
926
927/// Locator for determining which mini arena contains a point.
928pub struct MiniArenaLocator {
929    /// The index number of the mini arena. None if the point is not in a mini arena.
930    my_idx: Option<u8>,
931}
932
933impl MiniArenaLocator {
934    /// Create a locator for a specific mini arena index.
935    pub fn from_mini_arena_idx(val: u8) -> Self {
936        Self { my_idx: Some(val) }
937    }
938
939    /// Create a locator indicating no mini arena.
940    pub fn new_none() -> Self {
941        Self { my_idx: None }
942    }
943
944    /// Return the index number of the mini arena. None if the point is not in a
945    /// mini arena.
946    pub fn idx(&self) -> Option<u8> {
947        self.my_idx
948    }
949}
950
951/// Configuration defining potential mini arenas.
952#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
953#[serde(tag = "type")]
954pub enum MiniArenaConfig {
955    /// No mini arena is in use.
956    #[default]
957    NoMiniArena,
958    /// A 2D grid arranged along the X and Y axes.
959    XYGrid(XYGridConfig),
960}
961
962#[expect(clippy::len_without_is_empty)]
963impl MiniArenaConfig {
964    fn is_none(&self) -> bool {
965        self == &Self::NoMiniArena
966    }
967
968    /// Iterate over all mini arena locators in this configuration.
969    pub fn iter_locators(&self) -> impl Iterator<Item = MiniArenaLocator> + use<> {
970        let res = match self {
971            Self::NoMiniArena => vec![MiniArenaLocator::from_mini_arena_idx(0)],
972            Self::XYGrid(xy_grid_config) => {
973                let sz = xy_grid_config.x_centers.0.len() * xy_grid_config.y_centers.0.len();
974                (0..sz)
975                    .map(|idx| MiniArenaLocator::from_mini_arena_idx(idx.try_into().unwrap()))
976                    .collect()
977            }
978        };
979        res.into_iter()
980    }
981
982    /// Get the number of mini arenas in this configuration.
983    pub fn len(&self) -> usize {
984        match self {
985            Self::NoMiniArena => 1,
986            Self::XYGrid(xy_grid_config) => {
987                xy_grid_config.x_centers.0.len() * xy_grid_config.y_centers.0.len()
988            }
989        }
990    }
991}
992
993/// Sorted list of floating point values for efficient nearest neighbor search.
994#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
995struct Sorted(Vec<f64>);
996
997impl Sorted {
998    fn new(vals: &[f64]) -> Self {
999        assert!(!vals.is_empty());
1000        let mut vals: Vec<NotNan<f64>> = vals.iter().map(|v| NotNan::new(*v).unwrap()).collect();
1001        vals.sort();
1002        let vals = vals.iter().map(|v| v.into_inner()).collect();
1003        Sorted(vals)
1004    }
1005    fn dist_and_argmin(&self, x: f64) -> (f64, usize) {
1006        let mut best_dist = f64::INFINITY;
1007        let mut prev_dist = f64::INFINITY;
1008        let mut best_idx = 0;
1009        for (i, selfi) in self.0.iter().enumerate() {
1010            let dist = (selfi - x).abs();
1011            if dist < best_dist {
1012                best_dist = dist;
1013                best_idx = i;
1014            }
1015            if dist > prev_dist {
1016                // short circuit end of loop
1017                break;
1018            }
1019            prev_dist = dist
1020        }
1021        (best_dist, best_idx)
1022    }
1023}
1024
1025#[test]
1026fn test_sorted() {
1027    let x = Sorted::new(&[1.0, 2.0, 1.0]);
1028
1029    assert_eq!(x.0, vec![1.0, 1.0, 2.0]);
1030    assert_eq!(x.dist_and_argmin(1.1).1, 0);
1031
1032    let x = Sorted::new(&[1.0, 2.0, 1.0, 3.0, 4.0]);
1033    assert_eq!(x.dist_and_argmin(2.1).1, 2);
1034
1035    assert_eq!(x.dist_and_argmin(1.9).1, 2);
1036}
1037
1038/// Parameters defining a 2D grid of mini arenas arranged along X and Y axes.
1039#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1040pub struct XYGridConfig {
1041    x_centers: Sorted,
1042    y_centers: Sorted,
1043    radius: f64,
1044}
1045
1046impl XYGridConfig {
1047    /// Create a new XYGrid configuration with specified centers and radius.
1048    pub fn new(x: &[f64], y: &[f64], radius: f64) -> Self {
1049        Self {
1050            x_centers: Sorted::new(x),
1051            y_centers: Sorted::new(y),
1052            radius,
1053        }
1054    }
1055
1056    /// Iterate over all grid center coordinates.
1057    pub fn iter_centers(&self) -> impl Iterator<Item = (f64, f64)> + use<> {
1058        XYGridIter {
1059            col_centers: self.x_centers.0.clone(),
1060            row_centers: self.y_centers.0.clone(),
1061            next_idx: 0,
1062        }
1063    }
1064
1065    /// Get the arena index for given 3D coordinates.
1066    pub fn get_arena_index(&self, coords: &[MyFloat; 3]) -> MiniArenaLocator {
1067        if coords[2] != 0.0 {
1068            return MiniArenaLocator::new_none();
1069        }
1070        let obj_x = coords[0];
1071        let obj_y = coords[1];
1072
1073        let (dist_x, idx_x) = self.x_centers.dist_and_argmin(obj_x);
1074        let (dist_y, idx_y) = self.y_centers.dist_and_argmin(obj_y);
1075
1076        let dist = (dist_x * dist_x + dist_y * dist_y).sqrt();
1077        if dist <= self.radius {
1078            let idx = (idx_y * self.x_centers.0.len() + idx_x).try_into().unwrap();
1079            MiniArenaLocator::from_mini_arena_idx(idx)
1080        } else {
1081            MiniArenaLocator::new_none()
1082        }
1083    }
1084}
1085
1086struct XYGridIter {
1087    row_centers: Vec<f64>,
1088    col_centers: Vec<f64>,
1089    next_idx: usize,
1090}
1091
1092impl Iterator for XYGridIter {
1093    type Item = (f64, f64);
1094    fn next(&mut self) -> Option<Self::Item> {
1095        let (row_idx, col_idx) = num_integer::div_rem(self.next_idx, self.col_centers.len());
1096        if row_idx >= self.row_centers.len() {
1097            None
1098        } else {
1099            let result = (self.col_centers[col_idx], self.row_centers[row_idx]);
1100            self.next_idx += 1;
1101            Some(result)
1102        }
1103    }
1104}
1105
1106fn default_num_observations_to_visibility() -> u8 {
1107    // This number should suppress spurious trajectory births but not wait too
1108    // long before notifying listeners.
1109    3
1110}
1111
1112/// Floating point type used for coordinates.
1113pub type MyFloat = f64;
1114
1115/// Create default tracking parameters for full 3D tracking.
1116pub fn default_tracking_params_full_3d() -> TrackingParams {
1117    TrackingParams {
1118        motion_noise_scale: 0.1,
1119        initial_position_std_meters: 0.1,
1120        initial_vel_std_meters_per_sec: 1.0,
1121        accept_observation_min_likelihood: 1e-8,
1122        ekf_observation_covariance_pixels: 1.0,
1123        max_position_std_meters: 0.01212,
1124        hypothesis_test_params: Some(make_hypothesis_test_full3d_default()),
1125        num_observations_to_visibility: default_num_observations_to_visibility(),
1126        mini_arena_config: MiniArenaConfig::NoMiniArena,
1127    }
1128}
1129
1130/// Create default tracking parameters for flat 3D tracking.
1131pub fn default_tracking_params_flat_3d() -> TrackingParams {
1132    TrackingParams {
1133        motion_noise_scale: 0.0005,
1134        initial_position_std_meters: 0.001,
1135        initial_vel_std_meters_per_sec: 0.02,
1136        accept_observation_min_likelihood: 0.00001,
1137        ekf_observation_covariance_pixels: 1.0,
1138        max_position_std_meters: 0.003,
1139        hypothesis_test_params: None,
1140        num_observations_to_visibility: 10,
1141        mini_arena_config: MiniArenaConfig::NoMiniArena,
1142    }
1143}
1144
1145/// Parameters for hypothesis testing in track initialization.
1146#[derive(Debug, Clone, Serialize, Deserialize)]
1147pub struct HypothesisTestParams {
1148    /// Minimum number of cameras required for track initialization.
1149    pub minimum_number_of_cameras: u8,
1150    /// Maximum acceptable error in hypothesis testing.
1151    pub hypothesis_test_max_acceptable_error: f64,
1152    /// Minimum pixel absolute z-score threshold.
1153    pub minimum_pixel_abs_zscore: f64,
1154}
1155
1156/// Create default hypothesis testing parameters for full 3D tracking.
1157pub fn make_hypothesis_test_full3d_default() -> HypothesisTestParams {
1158    HypothesisTestParams {
1159        minimum_number_of_cameras: 2,
1160        hypothesis_test_max_acceptable_error: 5.0,
1161        minimum_pixel_abs_zscore: 0.0,
1162    }
1163}
1164
1165/// Information about a connected camera.
1166#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1167pub struct CamInfo {
1168    /// The raw camera name.
1169    pub name: RawCamName,
1170    /// The camera's synchronization state.
1171    pub state: ConnectedCameraSyncState,
1172    /// HTTP server information for the camera.
1173    pub strand_cam_http_server_info: BuiServerInfo,
1174    /// Recent statistics for the camera.
1175    pub recent_stats: RecentStats,
1176}
1177
1178/// API callback messages sent to Braid.
1179#[derive(Clone, Debug, Serialize, Deserialize)]
1180pub enum BraidHttpApiCallback {
1181    /// Called from strand-cam to register a camera
1182    ///
1183    /// Note this is different than the `cam_info_handler` which only queries
1184    /// for the appropriate camera configuration.
1185    NewCamera(RegisterNewCamera),
1186    /// Called from strand-cam to update the current image
1187    UpdateCurrentImage(PerCam<UpdateImage>),
1188    /// Called from strand-cam to update the current camera settings (e.g.
1189    /// exposure time)
1190    UpdateCamSettings(PerCam<UpdateCamSettings>),
1191    /// Called from strand-cam to update the current feature detection settings
1192    /// (e.g. threshold different)
1193    UpdateFeatureDetectSettings(PerCam<UpdateFeatureDetectSettings>),
1194    /// Start or stop recording data (.braid directory with csv tables for later
1195    /// .braidz file)
1196    DoRecordCsvTables(bool),
1197    /// Start or stop recording MKV videos for all cameras
1198    DoRecordMp4Files(bool),
1199    /// set uuid in the experiment_info table
1200    SetExperimentUuid(String),
1201    /// Set the number of frames to buffer in each camera
1202    SetPostTriggerBufferSize(usize),
1203    /// Initiate MKV recording using post trigger
1204    PostTriggerMp4Recording,
1205    /// Take a new background image on all cameras.
1206    ///
1207    /// Each camera re-initializes the background model used for feature
1208    /// detection from its currently incoming images.
1209    DoTakeNewBackgroundImage,
1210    /// Enable or disable continuous background model updating on all cameras.
1211    SetBackgroundUpdating(bool),
1212    /// Quit Braid.
1213    ///
1214    /// This stops recording (closing all files), commands all connected
1215    /// cameras to quit, and then exits.
1216    DoQuit,
1217}
1218
1219/// Wrapper for per-camera data.
1220#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
1221pub struct PerCam<T> {
1222    /// The raw camera name.
1223    pub raw_cam_name: RawCamName,
1224    /// The wrapped data.
1225    pub inner: T,
1226}
1227
1228/// Raw UDP packet containing 2D feature detections.
1229#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1230#[serde(deny_unknown_fields)]
1231pub struct FlydraRawUdpPacket {
1232    /// The name of the camera
1233    ///
1234    /// Traditionally this was the ROS camera name (e.g. with '-' converted to
1235    /// '_'), but have transitioned to allowing any valid UTF-8 string.
1236    pub cam_name: String,
1237    /// frame timestamp of trigger pulse start (or None if cannot be determined)
1238    #[serde(with = "crate::timestamp_opt_f64")]
1239    pub timestamp: Option<FlydraFloatTimestampLocal<Triggerbox>>,
1240    /// frame timestamp of camnode program sampling system clock
1241    #[serde(with = "crate::timestamp_f64")]
1242    pub cam_received_time: FlydraFloatTimestampLocal<HostClock>,
1243    /// timestamp from the camera
1244    pub device_timestamp: Option<u64>,
1245    /// frame number from the camera
1246    pub block_id: Option<u64>,
1247    /// Frame number from the camera.
1248    pub framenumber: i32,
1249    /// Detected 2D points in the frame.
1250    pub points: Vec<FlydraRawUdpPoint>,
1251}
1252
1253mod synced_frame;
1254pub use synced_frame::SyncFno;
1255
1256mod cam_num;
1257pub use cam_num::CamNum;
1258
1259mod timestamp;
1260pub use crate::timestamp::{
1261    FlydraFloatTimestampLocal, HostClock, Source, Triggerbox, triggerbox_time,
1262};
1263
1264/// Timestamp serialization for f64 format.
1265pub mod timestamp_f64;
1266/// Timestamp serialization for optional f64 format.
1267pub mod timestamp_opt_f64;
1268
1269#[cfg(feature = "with-tokio-codec")]
1270mod tokio_cbor;
1271#[cfg(feature = "with-tokio-codec")]
1272pub use crate::tokio_cbor::CborPacketCodec;
1273
1274/// Error types for Flydra operations.
1275#[derive(thiserror::Error, Debug)]
1276pub enum FlydraTypesError {
1277    #[error("CBOR data")]
1278    /// CBOR data error.
1279    CborDataError,
1280    #[error("serde error")]
1281    /// Serialization/deserialization error.
1282    SerdeError,
1283    #[error("unexpected hypothesis testing parameters")]
1284    /// Unexpected hypothesis testing parameters.
1285    UnexpectedHypothesisTestingParameters,
1286    #[error("input too long")]
1287    /// Input data too long.
1288    InputTooLong,
1289    #[error("long string not implemented")]
1290    /// Long string handling not implemented.
1291    LongStringNotImplemented,
1292    #[error("{0}")]
1293    /// I/O error.
1294    IoError(#[from] std::io::Error),
1295    #[error("{0}")]
1296    /// UTF-8 encoding error.
1297    Utf8Error(#[from] std::str::Utf8Error),
1298    #[error("URL parse error")]
1299    /// URL parsing error.
1300    UrlParseError,
1301}
1302
1303/// Trigger clock information record for CSV output.
1304#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
1305pub struct TriggerClockInfoRow {
1306    // backward-incompatible changes here require a BRAID_SCHEMA bump (see its definition)
1307    #[serde(with = "crate::timestamp_f64")]
1308    /// Timestamp when recording started.
1309    pub start_timestamp: FlydraFloatTimestampLocal<HostClock>,
1310    /// Number of frames recorded.
1311    pub framecount: i64,
1312    /// Fraction of full framecount is tcnt/255
1313    /// Trigger counter value.
1314    pub tcnt: u8,
1315    #[serde(with = "crate::timestamp_f64")]
1316    /// Timestamp when recording stopped.
1317    pub stop_timestamp: FlydraFloatTimestampLocal<HostClock>,
1318}
1319
1320/// Configuration for Triggerbox V1 hardware synchronization.
1321#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1322#[serde(deny_unknown_fields)]
1323pub struct TriggerboxConfig {
1324    /// Device filename for the triggerbox.
1325    pub device_fname: String,
1326    /// Frame rate for synchronized recording.
1327    pub framerate: f32,
1328    #[serde(default = "default_query_dt")]
1329    /// Query interval for triggerbox status.
1330    pub query_dt: std::time::Duration,
1331    /// Maximum acceptable measurement error.
1332    pub max_triggerbox_measurement_error: Option<std::time::Duration>,
1333}
1334
1335impl std::default::Default for TriggerboxConfig {
1336    fn default() -> Self {
1337        Self {
1338            device_fname: "/dev/trig1".to_string(),
1339            framerate: 100.0,
1340            query_dt: default_query_dt(),
1341            // Make a relatively long default so that cameras will synchronize
1342            // even with relatively long delays. Users can always specify
1343            // tighter precision within a config file.
1344            max_triggerbox_measurement_error: Some(std::time::Duration::from_millis(20)),
1345        }
1346    }
1347}
1348
1349const fn default_query_dt() -> std::time::Duration {
1350    std::time::Duration::from_millis(1500)
1351}
1352
1353/// Configuration for PTP (Precision Time Protocol) synchronization.
1354#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1355#[serde(deny_unknown_fields)]
1356pub struct PtpSyncConfig {
1357    /// The period of the periodic signal.
1358    ///
1359    /// If this is set, it is transmitted to the cameras.
1360    pub periodic_signal_period_usec: Option<f64>,
1361}
1362
1363/// Configuration for fake synchronization (no real synchronization).
1364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1365#[serde(deny_unknown_fields)]
1366pub struct FakeSyncConfig {
1367    /// Simulated frame rate.
1368    pub framerate: f64,
1369}
1370
1371impl Default for FakeSyncConfig {
1372    fn default() -> Self {
1373        Self { framerate: 95.0 }
1374    }
1375}
1376
1377/// Camera synchronization method configuration.
1378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1379#[serde(deny_unknown_fields)]
1380#[serde(tag = "trigger_type")]
1381pub enum TriggerType {
1382    /// Cameras are synchronized via hardware triggers controlled
1383    /// via a [Straw Lab triggerbox](https://github.com/strawlab/triggerbox).
1384    TriggerboxV1(TriggerboxConfig),
1385    /// Cameras are synchronized using PTP (Precision Time Protocol, IEEE 1588).
1386    PtpSync(PtpSyncConfig),
1387    /// Cameras are synchronized using device timestamps.
1388    DeviceTimestamp,
1389    /// Cameras are not synchronized, but we pretend they are.
1390    FakeSync(FakeSyncConfig),
1391}
1392
1393impl Default for TriggerType {
1394    fn default() -> Self {
1395        TriggerType::FakeSync(FakeSyncConfig::default())
1396    }
1397}
1398
1399/// Feature detection data in raw camera coordinates.
1400///
1401/// Because these are in raw camera coordinates (and thus have not been
1402/// undistorted with any lens distortion model), they are called "distorted".
1403///
1404/// Note that in `.braidz` files, subsequent rows on disk are not in general
1405/// monotonically increasing in frame number.
1406///
1407/// See the "Details about how data are processed online and saved for later
1408/// analysis" section in the "3D Tracking in Braid" chapter of the [User's
1409/// Guide](https://strawlab.github.io/strand-braid/) for a description of why
1410/// these cannot be relied upon in `.braidz` files to be monotonic.
1411#[derive(Clone, Debug, Serialize, Deserialize)]
1412pub struct Data2dDistortedRow {
1413    // backward-incompatible changes here require a BRAID_SCHEMA bump (see its definition)
1414    // should be kept in sync with Data2dDistortedRowF32
1415    /// The number of the camera.
1416    pub camn: CamNum,
1417    /// The synchronized frame number.
1418    ///
1419    /// This is very likely to be different than [Self::block_id], the camera's
1420    /// internal frame number, because Braid synchronizes the frames so that,
1421    /// e.g. "frame 10" occurred at the same instant across all cameras.
1422    pub frame: i64,
1423    /// This is the trigger timestamp (if available).
1424    #[serde(with = "crate::timestamp_opt_f64")]
1425    pub timestamp: Option<FlydraFloatTimestampLocal<Triggerbox>>,
1426    #[serde(with = "crate::timestamp_f64")]
1427    /// Timestamp when the camera received the frame.
1428    pub cam_received_timestamp: FlydraFloatTimestampLocal<HostClock>,
1429    /// Timestamp from the camera.
1430    pub device_timestamp: Option<u64>,
1431    /// Frame number from the camera.
1432    ///
1433    /// Note that this is not the synchronized frame number, which is [Self::frame].
1434    pub block_id: Option<u64>,
1435    /// The X (horizontal) coordinate of the detection, in camera pixels.
1436    #[serde(deserialize_with = "invalid_nan")]
1437    pub x: f64,
1438    /// The Y (vertical) coordinate of the detection, in camera pixels.
1439    #[serde(deserialize_with = "invalid_nan")]
1440    pub y: f64,
1441    /// The area of the detection, in camera pixels^2.
1442    #[serde(deserialize_with = "invalid_nan")]
1443    pub area: f64,
1444    /// The slope of the detection.
1445    ///
1446    /// The orientation, modulo 𝜋, of the detection, is `atan(slope)`.
1447    #[serde(deserialize_with = "invalid_nan")]
1448    pub slope: f64,
1449    /// The eccentricity of the detection.
1450    #[serde(deserialize_with = "invalid_nan")]
1451    pub eccentricity: f64,
1452    /// The index of this particular detection within a given frame.
1453    ///
1454    /// Multiple detections can occur within a single frame, and each succesive
1455    /// detection will have a higher index.
1456    pub frame_pt_idx: u8,
1457    /// Current pixel value.
1458    pub cur_val: u8,
1459    #[serde(deserialize_with = "invalid_nan")]
1460    /// Mean pixel value.
1461    pub mean_val: f64,
1462    #[serde(deserialize_with = "invalid_nan")]
1463    /// Sum of squares of pixel values.
1464    pub sumsqf_val: f64,
1465}
1466
1467/// Lower precision version of [Data2dDistortedRow] for saving to disk.
1468// Note that this matches the precision specified in the old flydra Python
1469// module `flydra_core.data_descriptions.Info2D`.
1470#[derive(Debug, Serialize)]
1471pub struct Data2dDistortedRowF32 {
1472    // backward-incompatible changes here require a BRAID_SCHEMA bump (see its definition)
1473    /// The number of the camera.
1474    pub camn: CamNum,
1475    /// The synchronized frame number.
1476    ///
1477    /// This is very likely to be different than [Self::block_id], the camera's
1478    /// internal frame number, because Braid synchronizes the frames so that,
1479    /// e.g. "frame 10" occurred at the same instant across all cameras.
1480    pub frame: i64,
1481    /// This is the trigger timestamp (if available).
1482    #[serde(with = "crate::timestamp_opt_f64")]
1483    pub timestamp: Option<FlydraFloatTimestampLocal<Triggerbox>>,
1484    #[serde(with = "crate::timestamp_f64")]
1485    /// Timestamp when the camera received the frame.
1486    pub cam_received_timestamp: FlydraFloatTimestampLocal<HostClock>,
1487    /// timestamp from the camera
1488    pub device_timestamp: Option<u64>,
1489    /// Frame number from the camera.
1490    ///
1491    /// Note that this is not the synchronized frame number, which is [Self::frame].
1492    pub block_id: Option<u64>,
1493    /// The X (horizontal) coordinate of the detection, in camera pixels.
1494    pub x: f32,
1495    /// The Y (vertial) coordinate of the detection, in camera pixels.
1496    pub y: f32,
1497    /// The area of the detection, in camera pixels^2.
1498    pub area: f32,
1499    /// The slope of the detection.
1500    ///
1501    /// The orientation, modulo 𝜋, of the detection, is `atan(slope)`.
1502    pub slope: f32,
1503    /// The eccentricity of the detection.
1504    pub eccentricity: f32,
1505    /// The index of this particular detection within a given frame.
1506    ///
1507    /// Multiple detections can occur within a single frame, and each succesive
1508    /// detection will have a higher index.
1509    pub frame_pt_idx: u8,
1510    /// Current pixel value.
1511    pub cur_val: u8,
1512    /// Mean pixel value.
1513    pub mean_val: f32,
1514    /// Sum of squares of pixel values.
1515    pub sumsqf_val: f32,
1516}
1517
1518impl From<Data2dDistortedRow> for Data2dDistortedRowF32 {
1519    fn from(orig: Data2dDistortedRow) -> Self {
1520        Self {
1521            camn: orig.camn,
1522            frame: orig.frame,
1523            timestamp: orig.timestamp,
1524            cam_received_timestamp: orig.cam_received_timestamp,
1525            device_timestamp: orig.device_timestamp,
1526            block_id: orig.block_id,
1527            x: orig.x as f32,
1528            y: orig.y as f32,
1529            area: orig.area as f32,
1530            slope: orig.slope as f32,
1531            eccentricity: orig.eccentricity as f32,
1532            frame_pt_idx: orig.frame_pt_idx,
1533            cur_val: orig.cur_val,
1534            mean_val: orig.mean_val as f32,
1535            sumsqf_val: orig.sumsqf_val as f32,
1536        }
1537    }
1538}
1539
1540impl WithKey<i64> for Data2dDistortedRow {
1541    fn key(&self) -> i64 {
1542        self.frame
1543    }
1544}
1545
1546fn invalid_nan<'de, D>(de: D) -> Result<f64, D::Error>
1547where
1548    D: Deserializer<'de>,
1549{
1550    f64::deserialize(de).or(
1551        // TODO: should match on DeserializeError with empty field only,
1552        // otherwise, return error. The way this is written, anything
1553        // will return a nan.
1554        Ok(f64::NAN),
1555    )
1556}
1557
1558/// URL path for Braid events endpoint.
1559pub const BRAID_EVENTS_URL_PATH: &str = "braid-events";
1560/// Event name for Braid events.
1561pub const BRAID_EVENT_NAME: &str = "braid";
1562/// Event name for the server-is-quitting SSE message.
1563///
1564/// Broadcast to every connected browser just before the server shuts down so
1565/// that all clients (not only the one that initiated the quit) show the "Braid
1566/// has quit" screen and stop trying to reconnect.
1567pub const BRAID_QUIT_EVENT_NAME: &str = "braid-quit";