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";