Skip to main content

strand_cam/
strand-cam.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// TODO: if camera not available, launch alternate UI indicating such and
5// waiting for it to become available?
6
7// TODO: add quit app button to UI.
8
9// TODO: UI automatically reconnect to app after app restart.
10
11use async_change_tracker::ChangeTracker;
12use event_stream_types::{
13    AcceptsEventStream, ConnectionEvent, ConnectionEventType, ConnectionSessionKey,
14    EventBroadcaster, TolerantJson,
15};
16use futures::stream::StreamExt;
17use http::StatusCode;
18use strand_http_video_streaming as video_streaming;
19
20use machine_vision_formats as formats;
21use preferences_serde1::{AppInfo, Preferences};
22use serde::{Deserialize, Serialize};
23use tokio::sync::mpsc::error::SendError;
24use tracing::{debug, error, info, trace, warn};
25
26use ci2::{Camera, CameraInfo, CameraModule, DynamicFrameWithInfo};
27use ci2_async::AsyncCamera;
28use fmf::FMFWriter;
29use formats::PixFmt;
30use strand_bui_backend_session_types::{BuiServerAddrInfo, ConnectionKey};
31use strand_dynamic_frame::DynamicFrame;
32
33use video_streaming::AnnotatedFrame;
34
35use std::{path::PathBuf, pin::Pin, result::Result as StdResult};
36
37/// Map [`ci2::Error::FeatureNotPresent`] to a fallback value, propagating any
38/// other error. Backends such as the webcam backend report controls they
39/// cannot provide this way; using a fallback lets the startup path degrade
40/// gracefully instead of aborting when a control is unavailable.
41fn feature_or<T>(result: ci2::Result<T>, fallback: T) -> ci2::Result<T> {
42    match result {
43        Err(ci2::Error::FeatureNotPresent()) => Ok(fallback),
44        other => other,
45    }
46}
47
48#[cfg(feature = "flydra_feat_detect")]
49use strand_cam_remote_control::CsvSaveConfig;
50use strand_cam_remote_control::{
51    CamArg, CodecSelection, FfmpegCodecArgs, FfmpegRecordingConfig, Mp4Codec, Mp4RecordingConfig,
52    NvidiaH264Options, RecordingFrameRate,
53};
54
55use braid_types::{BuiServerInfo, RawCamName, StartSoftwareFrameRateLimit, TriggerType};
56
57use flydra_feature_detector_types::ImPtDetectCfg;
58
59#[cfg(feature = "flydra_feat_detect")]
60use strand_cam_csv_config_types::CameraCfgFview2_0_26;
61
62#[cfg(feature = "fiducial")]
63use strand_cam_storetype::ApriltagState;
64use strand_cam_storetype::{
65    CallbackType, ImOpsState, RangedValue, STRAND_CAM_EVENT_NAME, StoreType, ToLedBoxDevice,
66};
67
68use strand_cam_storetype::{KalmanTrackingConfig, LedProgramConfig};
69
70use std::{
71    io::Write,
72    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket},
73    sync::{Arc, RwLock},
74};
75
76pub const APP_INFO: AppInfo = AppInfo {
77    name: "strand-cam",
78    author: "AndrewStraw",
79};
80
81pub use flydra_pt_detect_cfg::default_absdiff as default_im_pt_detect;
82
83#[cfg(feature = "bundle_files")]
84static ASSETS_DIR: include_dir::Dir<'static> =
85    include_dir::include_dir!("$CARGO_MANIFEST_DIR/yew_frontend/dist");
86
87#[cfg(feature = "flydratrax")]
88const KALMAN_TRACKING_PREFS_KEY: &str = "kalman-tracking";
89
90#[cfg(feature = "flydratrax")]
91const LED_PROGRAM_PREFS_KEY: &str = "led-config";
92
93const COOKIE_SECRET_KEY: &str = "cookie-secret-base64";
94const BRAID_COOKIE_KEY: &str = "braid-cookie";
95
96#[cfg(feature = "flydratrax")]
97mod flydratrax_handle_msg;
98
99mod clock_model;
100mod datagram_socket;
101mod post_trigger_buffer;
102
103#[cfg(feature = "eframe-gui")]
104mod gui_app;
105
106mod frame_process_task;
107
108mod cam_arg_task;
109mod cam_stream_task;
110mod http_router;
111mod led_box_task;
112use frame_process_task::frame_process_task;
113
114#[cfg(feature = "eframe-gui")]
115#[derive(Default)]
116struct GuiShared {
117    ctx: Option<eframe::egui::Context>,
118    url: Option<String>,
119}
120
121#[cfg(feature = "eframe-gui")]
122type ArcMutGuiSingleton = Arc<std::sync::Mutex<GuiShared>>;
123
124#[cfg(not(feature = "eframe-gui"))]
125type ArcMutGuiSingleton = ();
126
127pub mod cli_app;
128
129const LED_BOX_HEARTBEAT_INTERVAL_MSEC: u64 = 5000;
130
131use eyre::{Result, WrapErr, eyre};
132
133pub(crate) enum Msg {
134    StartMp4,
135    StopMp4,
136    StartFMF((String, RecordingFrameRate)),
137    StopFMF,
138    #[cfg(feature = "flydra_feat_detect")]
139    StartUFMF(String),
140    #[cfg(feature = "flydra_feat_detect")]
141    StopUFMF,
142    #[cfg(feature = "flydra_feat_detect")]
143    SetTracking(bool),
144    PostTriggerStartMp4,
145    SetPostTriggerBufferSize(usize),
146    Mframe(DynamicFrameWithInfo),
147    #[cfg(feature = "flydra_feat_detect")]
148    SetIsSavingObjDetectionCsv(CsvSaveConfig),
149    #[cfg(feature = "flydra_feat_detect")]
150    SetExpConfig(ImPtDetectCfg),
151    Store(Arc<RwLock<ChangeTracker<StoreType>>>),
152    #[cfg(feature = "flydra_feat_detect")]
153    TakeCurrentImageAsBackground,
154    #[cfg(feature = "flydra_feat_detect")]
155    ClearBackground(f32),
156    SetFrameOffset(u64),
157    SetTriggerboxClockModel(Option<strand_cam_bui_types::ClockModel>),
158    StartAprilTagRec(String),
159    StopAprilTagRec,
160}
161
162impl std::fmt::Debug for Msg {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> {
164        write!(f, "strand_cam::Msg{{..}}")
165    }
166}
167
168#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
169pub enum FrameProcessingErrorState {
170    #[default]
171    NotifyAll,
172    IgnoreUntil(chrono::DateTime<chrono::Utc>),
173    IgnoreAll,
174}
175
176#[cfg(feature = "flydra_feat_detect")]
177#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
178pub enum Tracker {
179    NoTracker,
180    BackgroundSubtraction(ImPtDetectCfg),
181}
182
183/// Which timestamp source is used to estimate the frame rate.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum FpsTimestampSource {
186    /// A trigger-derived timestamp (external trigger box, PTP sync, or the
187    /// device clock mapped through a clock model). Most preferred: it reflects
188    /// the true acquisition time and is available whenever a trigger is
189    /// configured — including for cameras that expose no usable raw hardware
190    /// timestamp — so it is usable in more cases than the device timestamp.
191    Trigger,
192    /// The camera's hardware/device timestamp. Used when no trigger is in use.
193    /// It reflects the true acquisition time and is immune to host-side
194    /// buffering, so the estimate is correct even when frames are delivered to
195    /// the host in bursts.
196    Hardware,
197    /// The host clock sampled when the frame was grabbed. Last-resort fallback
198    /// used when neither a trigger timestamp nor a camera hardware timestamp is
199    /// available. Under load the driver buffers frames and the host grabs them
200    /// in bursts, so this can read several times too high.
201    HostClock,
202}
203
204pub struct FpsCalc {
205    /// (frame number, timestamp in nanoseconds) of the last sample.
206    prev: Option<(usize, i128)>,
207    frames_to_average: usize,
208    /// The source of the current running sample. Only differences within a
209    /// single source are meaningful (epochs differ between sources), so the
210    /// estimate is reset whenever the source changes.
211    source: Option<FpsTimestampSource>,
212}
213
214impl FpsCalc {
215    /// create a new FpsCalc instance
216    pub fn new(frames_to_average: usize) -> Self {
217        Self {
218            prev: None,
219            frames_to_average,
220            source: None,
221        }
222    }
223    /// Update with a frame number and a timestamp (nanoseconds) from `source`,
224    /// returning a newly computed fps whenever available.
225    ///
226    /// The timestamp epoch is irrelevant (only differences are used), but it
227    /// must be consistent within a source, so the running estimate restarts
228    /// whenever `source` changes.
229    pub fn update(
230        &mut self,
231        fno: usize,
232        stamp_nanos: i128,
233        source: FpsTimestampSource,
234    ) -> Option<f64> {
235        if self.source != Some(source) {
236            // Source changed (or first sample): restart the estimate.
237            self.source = Some(source);
238            self.prev = Some((fno, stamp_nanos));
239            return None;
240        }
241        let mut reset_previous = true;
242        let mut result = None;
243        if let Some((prev_frame, prev_stamp)) = self.prev {
244            let n_frames = fno - prev_frame;
245            if n_frames < self.frames_to_average {
246                reset_previous = false;
247            } else {
248                let dur_nsec = stamp_nanos - prev_stamp;
249                if dur_nsec > 0 {
250                    result = Some(n_frames as f64 / dur_nsec as f64 * 1.0e9);
251                }
252            }
253        }
254        if reset_previous {
255            self.prev = Some((fno, stamp_nanos));
256        }
257        result
258    }
259}
260
261#[cfg(test)]
262mod fps_calc_tests {
263    use super::{FpsCalc, FpsTimestampSource};
264
265    const NS_PER_S: i128 = 1_000_000_000;
266
267    /// With the camera hardware timestamp (true cadence), the estimated rate is
268    /// correct even when frames are delivered to the host in bursts. This is the
269    /// fix for the live-vs-retrack fragmentation bug.
270    #[test]
271    fn hardware_timestamp_is_robust_to_bursty_delivery() {
272        let mut fc = FpsCalc::new(100);
273        let true_fps = 30.0;
274        let dt_ns = (NS_PER_S as f64 / true_fps) as i128; // true inter-frame, ns
275        let mut measured = None;
276        for fno in 0..=100usize {
277            // Hardware timestamp advances at the true cadence regardless of when
278            // the host actually grabbed the frame.
279            let stamp = fno as i128 * dt_ns;
280            if let Some(fps) = fc.update(fno, stamp, FpsTimestampSource::Hardware) {
281                measured = Some(fps);
282            }
283        }
284        let fps = measured.expect("should produce an estimate after 100 frames");
285        assert!(
286            (fps - true_fps).abs() < 0.1,
287            "hardware fps {fps} != {true_fps}"
288        );
289    }
290
291    /// The trigger timestamp (the preferred source) is likewise robust to
292    /// bursty host-side delivery, since it reflects the true acquisition cadence
293    /// rather than when the host grabbed each frame.
294    #[test]
295    fn trigger_timestamp_is_robust_to_bursty_delivery() {
296        let mut fc = FpsCalc::new(100);
297        let true_fps = 30.0;
298        let dt_ns = (NS_PER_S as f64 / true_fps) as i128; // true inter-frame, ns
299        let mut measured = None;
300        for fno in 0..=100usize {
301            let stamp = fno as i128 * dt_ns;
302            if let Some(fps) = fc.update(fno, stamp, FpsTimestampSource::Trigger) {
303                measured = Some(fps);
304            }
305        }
306        let fps = measured.expect("should produce an estimate after 100 frames");
307        assert!(
308            (fps - true_fps).abs() < 0.1,
309            "trigger fps {fps} != {true_fps}"
310        );
311    }
312
313    /// Demonstrates the bug being fixed: with the host clock, a burst (e.g. the
314    /// driver delivering 100 buffered frames in ~1/3 of the true span) makes the
315    /// estimate read far too high.
316    #[test]
317    fn host_clock_overreads_under_bursty_delivery() {
318        let mut fc = FpsCalc::new(100);
319        let true_fps = 30.0;
320        // 100 frames truly span 100/30 s, but the host grabbed them bunched into
321        // 1/3 of that wall-clock time.
322        let bunched_span_ns = (100.0 / true_fps / 3.0 * NS_PER_S as f64) as i128;
323        let mut measured = None;
324        for fno in 0..=100usize {
325            let stamp = (fno as i128 * bunched_span_ns) / 100;
326            if let Some(fps) = fc.update(fno, stamp, FpsTimestampSource::HostClock) {
327                measured = Some(fps);
328            }
329        }
330        let fps = measured.unwrap();
331        assert!(fps > 80.0, "expected an inflated host-clock fps, got {fps}");
332    }
333
334    /// Changing the timestamp source restarts the estimate (epochs differ
335    /// between sources, so a cross-source delta would be meaningless).
336    #[test]
337    fn changing_source_resets_estimate() {
338        let mut fc = FpsCalc::new(2);
339        assert_eq!(fc.update(0, 0, FpsTimestampSource::Hardware), None);
340        // Switch source at frame 2: must not compute across the boundary.
341        assert_eq!(fc.update(2, 999_999, FpsTimestampSource::HostClock), None);
342        // Now consistent host-clock samples produce an estimate.
343        let dt = NS_PER_S / 30;
344        assert_eq!(fc.update(2, 0, FpsTimestampSource::HostClock), None);
345        let fps = fc.update(4, 2 * dt, FpsTimestampSource::HostClock).unwrap();
346        assert!((fps - 30.0).abs() < 0.5, "fps {fps}");
347    }
348}
349
350struct FmfWriteInfo<T>
351where
352    T: std::io::Write + std::io::Seek,
353{
354    writer: FMFWriter<T>,
355    recording_framerate: RecordingFrameRate,
356    last_saved_stamp: Option<chrono::DateTime<chrono::Utc>>,
357}
358
359impl<T> FmfWriteInfo<T>
360where
361    T: std::io::Write + std::io::Seek,
362{
363    fn new(writer: FMFWriter<T>, recording_framerate: RecordingFrameRate) -> Self {
364        Self {
365            writer,
366            recording_framerate,
367            last_saved_stamp: None,
368        }
369    }
370}
371
372#[cfg(feature = "checkercal")]
373type CollectedCornersArc = Arc<RwLock<Vec<Vec<(f32, f32)>>>>;
374
375async fn convert_stream(
376    raw_cam_name: RawCamName,
377    mut transmit_feature_detect_settings_rx: tokio::sync::mpsc::Receiver<
378        flydra_feature_detector_types::ImPtDetectCfg,
379    >,
380    transmit_msg_tx: tokio::sync::mpsc::Sender<braid_types::BraidHttpApiCallback>,
381) -> Result<()> {
382    while let Some(val) = transmit_feature_detect_settings_rx.recv().await {
383        let msg =
384            braid_types::BraidHttpApiCallback::UpdateFeatureDetectSettings(braid_types::PerCam {
385                raw_cam_name: raw_cam_name.clone(),
386                inner: braid_types::UpdateFeatureDetectSettings {
387                    current_feature_detect_settings: val,
388                },
389            });
390        transmit_msg_tx.send(msg).await?;
391    }
392    Ok(())
393}
394
395/// Find the local source IP address the OS would use to reach `remote_ip`.
396///
397/// Uses a UDP socket `connect()` (which performs endpoint association and
398/// routing/source-address selection without sending application payload) to ask
399/// the OS which local IP it would use for traffic to `remote_ip`.
400///
401/// Note: this is a local routing decision only. It does not guarantee that a
402/// remote peer can connect back (for example due to NAT or firewall rules).
403fn find_local_ip_for_remote(remote_ip: IpAddr) -> std::io::Result<IpAddr> {
404    let bind_addr: SocketAddr = match remote_ip {
405        IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
406        IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
407    };
408    let socket = UdpSocket::bind(bind_addr)?;
409    // Port 1 is arbitrary; UDP connect() only records the routing decision.
410    socket.connect(SocketAddr::new(remote_ip, 1))?;
411    Ok(socket.local_addr()?.ip())
412}
413
414/// Decide the HTTP server listen address that strand-cam should bind to and
415/// advertise to Braid when running in the Braid context.
416///
417/// Braid must be able to connect back to this strand-cam's HTTP server (see
418/// `docs/.../braid_remote_cameras.md`). The address is chosen as follows:
419///
420/// - If Braid's per-camera config provides an explicit `http_server_addr`
421///   override, always use it verbatim.
422/// - Otherwise, if Braid is on loopback, bind loopback (`127.0.0.1:0`).
423/// - Otherwise (Braid is remote), use `resolver` to find the local source IP
424///   the OS would use to reach Braid, so Braid can connect back to it. If that
425///   fails, warn and fall back to loopback (which will likely fail for a remote
426///   Braid, but is the safest local default).
427///
428/// `resolver` is injected (rather than calling [`find_local_ip_for_remote`]
429/// directly) so the decision logic can be unit-tested without real sockets.
430fn braid_strand_cam_http_address(
431    mainbrain_ip: IpAddr,
432    http_server_addr_override: Option<String>,
433    resolver: impl FnOnce(IpAddr) -> std::io::Result<IpAddr>,
434) -> String {
435    if mainbrain_ip.is_loopback() {
436        http_server_addr_override.unwrap_or_else(|| "127.0.0.1:0".to_string())
437    } else {
438        http_server_addr_override.unwrap_or_else(|| {
439            // When braid is on a different machine it must connect back to this
440            // strand-cam's HTTP server. Find the outgoing interface IP that
441            // braid can reach.
442            match resolver(mainbrain_ip) {
443                Ok(local_ip) => format!("{local_ip}:0"),
444                Err(e) => {
445                    tracing::warn!(
446                        "Could not determine local IP for braid at {mainbrain_ip}: {e}. \
447                        Falling back to 127.0.0.1, which may fail for remote connections."
448                    );
449                    "127.0.0.1:0".to_string()
450                }
451            }
452        })
453    }
454}
455
456fn open_braid_destination_addr(camdata_udp_addr: &SocketAddr) -> Result<UdpSocket> {
457    info!(
458        "Sending detected coordinates via UDP to: {}",
459        camdata_udp_addr
460    );
461
462    let timeout = std::time::Duration::new(0, 1);
463
464    let src_ip = if !camdata_udp_addr.ip().is_loopback() {
465        // Let OS choose what IP to use, but preserve V4 or V6.
466        match camdata_udp_addr {
467            SocketAddr::V4(_) => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
468            SocketAddr::V6(_) => IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)),
469        }
470    } else {
471        match camdata_udp_addr {
472            SocketAddr::V4(_) => IpAddr::V4(Ipv4Addr::LOCALHOST),
473            SocketAddr::V6(_) => IpAddr::V6(Ipv6Addr::LOCALHOST),
474        }
475    };
476    // Let OS choose what port to use.
477    let src_addr = SocketAddr::new(src_ip, 0);
478
479    let sock = UdpSocket::bind(src_addr)?;
480    sock.set_write_timeout(Some(timeout))?;
481    sock.connect(camdata_udp_addr)?;
482    Ok(sock)
483}
484
485#[cfg(feature = "flydra_feat_detect")]
486fn get_intensity(device_state: &strand_led_box_comms::DeviceState, chan_num: u8) -> u16 {
487    let ch: &strand_led_box_comms::ChannelState = match chan_num {
488        1 => &device_state.ch1,
489        2 => &device_state.ch2,
490        3 => &device_state.ch3,
491        c => panic!("unknown channel {c}"),
492    };
493    match ch.on_state {
494        strand_led_box_comms::OnState::Off => 0,
495        strand_led_box_comms::OnState::ConstantOn => ch.intensity,
496    }
497}
498
499/// Ignore a send error.
500///
501/// During shutdown, the receiver can disappear before the sender is closed.
502/// According to the docs of the `send` method of [tokio::sync::mpsc::Sender],
503/// this is the only way we can get a [tokio::sync::mpsc::error::SendError].
504/// Therefore, we ignore the error.
505trait IgnoreSendError {
506    fn ignore_send_error(self);
507}
508
509impl<T: std::fmt::Debug> IgnoreSendError for StdResult<(), tokio::sync::mpsc::error::SendError<T>> {
510    fn ignore_send_error(self) {
511        match self {
512            Ok(()) => {}
513            Err(e) => {
514                debug!("Ignoring send error ({}:{}): {:?}", file!(), line!(), e)
515            }
516        }
517    }
518}
519
520#[derive(Clone)]
521struct StrandCamCallbackSenders {
522    firehose_callback_tx: tokio::sync::mpsc::Sender<ConnectionKey>,
523    cam_args_tx: tokio::sync::mpsc::Sender<CamArg>,
524    led_box_tx_std: tokio::sync::mpsc::Sender<ToLedBoxDevice>,
525    #[cfg_attr(not(feature = "flydra_feat_detect"), expect(unused))]
526    tx_frame: tokio::sync::mpsc::Sender<Msg>,
527}
528
529#[derive(Clone)]
530struct StrandCamAppState {
531    cam_name: String,
532    event_broadcaster: EventBroadcaster<ConnectionSessionKey>,
533    callback_senders: StrandCamCallbackSenders,
534    tx_new_connection: tokio::sync::mpsc::Sender<event_stream_types::ConnectionEvent>,
535    shared_store_arc: Arc<RwLock<ChangeTracker<StoreType>>>,
536    /// The address the HTTP server is bound to (possibly unspecified, e.g.
537    /// `0.0.0.0`), used to enumerate device-connection URLs on demand.
538    bui_server_info: strand_bui_backend_session_types::BuiServerAddrInfo,
539    /// The cookie/token secret, used to mint a fresh short-lived access token
540    /// when a device-connection QR code is requested.
541    persistent_secret: cookie::Key,
542}
543
544fn display_qr_url(url: &str) -> Result<()> {
545    use qrcode::QrCode;
546    use qrcode::render::unicode;
547    use std::io::stdout;
548
549    let qr = QrCode::new(url)?;
550
551    let image = qr.render::<unicode::Dense1x2>().build();
552
553    let stdout = stdout();
554    let mut stdout_handle = stdout.lock();
555    writeln!(stdout_handle)?;
556    stdout_handle.write_all(image.as_bytes())?;
557    writeln!(stdout_handle)?;
558    Ok(())
559}
560
561#[derive(Debug, Clone)]
562/// Defines whether runtime changes from the user are persisted to disk.
563///
564/// If they are persisted to disk, upon program re-start, the disk
565/// is checked and preferences are loaded from there. If they cannot
566/// be loaded, the defaults are used.
567pub enum ImPtDetectCfgSource {
568    ChangesNotSavedToDisk(ImPtDetectCfg),
569    ChangedSavedToDisk((&'static AppInfo, String)),
570}
571
572#[cfg(feature = "flydra_feat_detect")]
573impl Default for ImPtDetectCfgSource {
574    fn default() -> Self {
575        ImPtDetectCfgSource::ChangesNotSavedToDisk(default_im_pt_detect())
576    }
577}
578
579#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
580pub enum TimestampSource {
581    BraidTrigger, // TODO: rename to CleverComputation or similar
582    HostAcquiredTimestamp,
583}
584
585const MOMENT_CENTROID_SCHEMA_VERSION: u8 = 2;
586
587#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
588pub struct MomentCentroid {
589    pub schema_version: u8,
590    pub framenumber: u64,
591    pub timestamp_source: TimestampSource,
592    pub timestamp: chrono::DateTime<chrono::Utc>,
593    pub mu00: f32,
594    pub mu01: f32,
595    pub mu10: f32,
596    pub center_x: u32,
597    pub center_y: u32,
598    #[serde(default)]
599    pub cam_name: String,
600}
601
602#[derive(Debug, Serialize, Deserialize)]
603enum CentroidToDevice {
604    Centroid(MomentCentroid),
605}
606
607/// CLI args for the case when we will connect to Braid.
608///
609/// Prior to the connection, we don't know much about what our configuration
610/// should be. This is very limited because most configuration should be done in
611/// the Braid configuration .toml file.
612#[derive(Debug, Default, Clone)]
613pub struct BraidArgs {
614    pub braid_url: String,
615    pub camera_name: String,
616}
617
618/// CLI args for the case when we run standalone.
619#[derive(Debug, Clone, Default)]
620pub struct StandaloneArgs {
621    pub camera_name: Option<String>,
622    /// The HTTP socket address for the Strand Cam BUI.
623    pub http_server_addr: Option<String>,
624    pub pixel_format: Option<String>,
625    /// If set, camera acquisition will external trigger.
626    pub force_camera_sync_mode: bool,
627    /// If enabled, limit framerate (FPS) at startup.
628    ///
629    /// Despite the name ("software"), this actually sets the hardware
630    /// acquisition rate via the `AcquisitionFrameRate` camera parameter.
631    pub software_limit_framerate: StartSoftwareFrameRateLimit,
632    /// Threshold duration before logging error (msec).
633    ///
634    /// If the image acquisition timestamp precedes the computed trigger
635    /// timestamp, clearly an error has happened. This error must lie in the
636    /// computation of the trigger timestamp. This specifies the threshold error
637    /// at which an error is logged. (The underlying source of such errors
638    /// remains unknown.)
639    pub acquisition_duration_allowed_imprecision_msec: Option<f64>,
640    /// Filename of vendor-specific camera settings file.
641    pub camera_settings_filename: Option<std::path::PathBuf>,
642    #[cfg(feature = "flydra_feat_detect")]
643    pub tracker_cfg_src: ImPtDetectCfgSource,
644}
645
646#[derive(Debug)]
647pub enum StandaloneOrBraid {
648    Standalone(StandaloneArgs),
649    Braid(BraidArgs),
650}
651
652impl Default for StandaloneOrBraid {
653    fn default() -> Self {
654        Self::Standalone(Default::default())
655    }
656}
657
658/// Default filename template for saved `.mp4` recordings.
659pub(crate) const MP4_FILENAME_TEMPLATE_DEFAULT: &str = "movie%Y%m%d_%H%M%S.%f_{CAMNAME}.mp4";
660/// Default filename template for saved `.fmf` recordings.
661pub(crate) const FMF_FILENAME_TEMPLATE_DEFAULT: &str = "movie%Y%m%d_%H%M%S.%f_{CAMNAME}.fmf";
662/// Default filename template for saved `.ufmf` recordings.
663pub(crate) const UFMF_FILENAME_TEMPLATE_DEFAULT: &str = "movie%Y%m%d_%H%M%S.%f_{CAMNAME}.ufmf";
664
665#[derive(Debug)]
666pub struct StrandCamArgs {
667    /// Is Strand Cam running inside Braid context?
668    pub standalone_or_braid: StandaloneOrBraid,
669    /// base64 encoded secret. minimum 256 bits.
670    pub secret: Option<String>,
671    /// Client networks (CIDR, e.g. `100.64.0.0/10`) trusted to have already
672    /// authenticated the peer (e.g. Tailscale/WireGuard). Requests from these
673    /// networks are accepted without an access token.
674    pub trusted_networks: Vec<String>,
675    pub no_browser: bool,
676    pub mp4_filename_template: String,
677    pub fmf_filename_template: String,
678    pub ufmf_filename_template: String,
679    pub disable_console: bool,
680    pub csv_save_dir: String,
681    pub led_box_device_path: Option<String>,
682    #[cfg(feature = "flydratrax")]
683    pub save_empty_data2d: SaveEmptyData2dType,
684    #[cfg(feature = "flydratrax")]
685    pub model_server_addr: std::net::SocketAddr,
686    #[cfg(feature = "flydratrax")]
687    pub flydratrax_calibration_source: CalSource,
688    #[cfg(feature = "fiducial")]
689    pub apriltag_csv_filename_template: String,
690    #[cfg(feature = "flydratrax")]
691    pub write_buffer_size_num_messages: usize,
692    #[cfg(target_os = "linux")]
693    v4l2loopback: Option<PathBuf>,
694    data_dir: Option<PathBuf>,
695}
696
697pub type SaveEmptyData2dType = bool;
698
699#[derive(Debug)]
700pub enum CalSource {
701    /// Use circular tracking region to create calibration
702    PseudoCal,
703    /// Use flydra .xml file with single camera for calibration
704    XmlFile(std::path::PathBuf),
705    /// Use pymvg .json file with single camera for calibration
706    PymvgJsonFile(std::path::PathBuf),
707}
708
709impl Default for StrandCamArgs {
710    fn default() -> Self {
711        Self {
712            standalone_or_braid: Default::default(),
713            secret: None,
714            trusted_networks: Vec::new(),
715            no_browser: true,
716            mp4_filename_template: MP4_FILENAME_TEMPLATE_DEFAULT.to_string(),
717            fmf_filename_template: FMF_FILENAME_TEMPLATE_DEFAULT.to_string(),
718            ufmf_filename_template: UFMF_FILENAME_TEMPLATE_DEFAULT.to_string(),
719            disable_console: false,
720            #[cfg(feature = "fiducial")]
721            apriltag_csv_filename_template: strand_cam_storetype::APRILTAG_CSV_TEMPLATE_DEFAULT
722                .to_string(),
723            csv_save_dir: "/dev/null".to_string(),
724            led_box_device_path: None,
725            #[cfg(feature = "flydratrax")]
726            flydratrax_calibration_source: CalSource::PseudoCal,
727            #[cfg(feature = "flydratrax")]
728            save_empty_data2d: true,
729            #[cfg(feature = "flydratrax")]
730            model_server_addr: braid_types::DEFAULT_MODEL_SERVER_ADDR.parse().unwrap(),
731            #[cfg(feature = "flydratrax")]
732            write_buffer_size_num_messages:
733                braid_config_data::default_write_buffer_size_num_messages(),
734            #[cfg(target_os = "linux")]
735            v4l2loopback: None,
736            data_dir: Default::default(),
737        }
738    }
739}
740
741fn test_nvenc_save(frame: DynamicFrame) -> Result<bool> {
742    let cfg = Mp4RecordingConfig {
743        codec: Mp4Codec::H264NvEnc(NvidiaH264Options {
744            bitrate: None,
745            cuda_device: 0,
746        }),
747        h264_metadata: None,
748        max_framerate: RecordingFrameRate::Fps30,
749    };
750    let mut nv_cfg_test = cfg.clone();
751
752    let libs = match nvenc::Dynlibs::new() {
753        Ok(libs) => libs,
754        Err(e) => {
755            debug!("nvidia NvEnc library could not be loaded: {:?}", e);
756            return Ok(false);
757        }
758    };
759
760    let opts = NvidiaH264Options {
761        bitrate: None,
762        ..Default::default()
763    };
764
765    nv_cfg_test.codec = strand_cam_remote_control::Mp4Codec::H264NvEnc(opts);
766
767    // Temporary variable to hold file data. This will be dropped
768    // at end of scope.
769    let mut buf = std::io::Cursor::new(Vec::new());
770
771    let nv_enc = match nvenc::NvEnc::new(&libs) {
772        Ok(nv_enc) => nv_enc,
773        Err(e) => {
774            debug!("nvidia NvEnc could not be initialized: {:?}", e);
775            return Ok(false);
776        }
777    };
778
779    let mut mp4_writer = mp4_writer::Mp4Writer::new(&mut buf, nv_cfg_test, Some(nv_enc))?;
780    match mp4_writer.write_dynamic(&frame, chrono::Local::now()) {
781        Ok(()) => {}
782        Err(e) => {
783            debug!("nvidia NvEnc could not be initialized: {:?}", e);
784            return Ok(false);
785        }
786    }
787    mp4_writer.finish()?;
788
789    debug!("MP4 video with nvenc h264 encoding succeeded.");
790
791    // When `buf` goes out of scope, it will be dropped.
792    Ok(true)
793}
794
795fn to_event_chunk(state: &StoreType) -> String {
796    let buf = serde_json::to_string(&state).unwrap();
797    format!("event: {STRAND_CAM_EVENT_NAME}\ndata: {buf}\n\n")
798}
799
800/// Handle a new connection to the event stream.
801///
802/// This creates a new channel which sends events to the new connection. The
803/// receiver side is simply the http body passed to axum. The sender side is
804/// initially started with a couple messages and then is ultimately sent to a
805/// "global event sender" which will send ongoing events to all connections.
806async fn events_handler(
807    axum::extract::State(app_state): axum::extract::State<StrandCamAppState>,
808    session_key: axum_token_auth::SessionKey,
809    axum::extract::ConnectInfo(addr): axum::extract::ConnectInfo<SocketAddr>,
810    _: AcceptsEventStream,
811) -> impl axum::response::IntoResponse {
812    session_key.is_present();
813    tracing::trace!("events");
814    // Connection wants to subscribe to event stream.
815
816    let key = ConnectionSessionKey::new(session_key.0, addr);
817
818    // Create a new channel in which the receiver is used to send responses to
819    // the new connection. The sender is sent to the app where it is stored in a
820    // per-connection map. Changes for this connection will then be sent to the
821    // stored sender.
822    let (conn_tx, body) = app_state.event_broadcaster.new_connection(key);
823
824    // Send the first message, the connection key.
825    {
826        let chunk = format!(
827            "event: {}\ndata: {}\n\n",
828            strand_cam_storetype::CONN_KEY_EVENT_NAME,
829            addr
830        );
831        match conn_tx.send(http_body::Frame::data(chunk.into())).await {
832            Ok(()) => {}
833            Err(tokio::sync::mpsc::error::SendError(_)) => {
834                // The receiver was dropped because the connection closed. Should probably do more here.
835                tracing::debug!("initial send error");
836            }
837        }
838    }
839
840    // Send the second message, a copy of our state.
841    {
842        let shared_store = app_state.shared_store_arc.read().unwrap().as_ref().clone();
843        let chunk = to_event_chunk(&shared_store);
844        match conn_tx.send(http_body::Frame::data(chunk.into())).await {
845            Ok(()) => {}
846            Err(tokio::sync::mpsc::error::SendError(_)) => {
847                // The receiver was dropped because the connection closed. Should probably do more here.
848                tracing::debug!("initial send error");
849            }
850        }
851    }
852
853    // Finally, send `tx`, the sender of the newly created channel, to the
854    // "global event sender" which will send further events to the connection.
855    {
856        let typ = ConnectionEventType::Connect(conn_tx);
857        let connection_key = ConnectionKey { addr };
858
859        match app_state
860            .tx_new_connection
861            .send(ConnectionEvent {
862                typ,
863                connection_key,
864            })
865            .await
866        {
867            Ok(()) => Ok(body),
868            Err(_) => Err((
869                StatusCode::INTERNAL_SERVER_ERROR,
870                "sending new connection failed",
871            )),
872        }
873    }
874}
875
876async fn cam_name_handler(
877    axum::extract::State(app_state): axum::extract::State<StrandCamAppState>,
878    session_key: axum_token_auth::SessionKey,
879) -> impl axum::response::IntoResponse {
880    session_key.is_present();
881    app_state.cam_name.clone()
882}
883
884/// Returns the URLs (one per reachable network interface) at which this web UI
885/// can be reached, each carrying a freshly minted short-lived access token. The
886/// frontend turns these into QR codes for connecting another device.
887async fn device_connect_urls_handler(
888    axum::extract::State(app_state): axum::extract::State<StrandCamAppState>,
889    session_key: axum_token_auth::SessionKey,
890) -> impl axum::response::IntoResponse {
891    session_key.is_present();
892    build_device_connect_urls(&app_state.bui_server_info, &app_state.persistent_secret)
893}
894
895/// Build the [`DeviceConnectUrls`] response: enumerate the interfaces the server
896/// is reachable on and mint a fresh access token (unless bound to loopback).
897///
898/// [`DeviceConnectUrls`]: strand_bui_backend_session_types::DeviceConnectUrls
899fn build_device_connect_urls(
900    bui_server_info: &strand_bui_backend_session_types::BuiServerAddrInfo,
901    persistent_secret: &cookie::Key,
902) -> axum::response::Response {
903    use axum::response::IntoResponse;
904    use strand_bui_backend_session_types::{AccessToken, BuiServerAddrInfo, DeviceConnectUrls};
905
906    let bound = *bui_server_info.addr();
907    // Match the token policy of `braid_types::start_listener`: a token is only
908    // required (and only useful) when the server is not bound to loopback.
909    let token = if bound.ip().is_loopback() {
910        AccessToken::NoToken
911    } else {
912        AccessToken::PreSharedToken(axum_token_auth::generate_token(
913            persistent_secret,
914            braid_types::ACCESS_TOKEN_TTL,
915        ))
916    };
917    let info = BuiServerAddrInfo::new(bound, token);
918    let uris = match strand_bui_backend_session::build_urls(&info) {
919        Ok(uris) => uris,
920        Err(e) => {
921            return (
922                axum::http::StatusCode::INTERNAL_SERVER_ERROR,
923                format!("failed to enumerate network interfaces: {e}"),
924            )
925                .into_response();
926        }
927    };
928    let loopback_only = uris.iter().all(braid_types::is_loopback);
929    let urls = uris.into_iter().map(|u| u.to_string()).collect();
930    axum::Json(DeviceConnectUrls {
931        urls,
932        loopback_only,
933    })
934    .into_response()
935}
936
937async fn callback_handler(
938    axum::extract::State(app_state): axum::extract::State<StrandCamAppState>,
939    session_key: axum_token_auth::SessionKey,
940    TolerantJson(payload): TolerantJson<CallbackType>,
941) -> axum::response::Response {
942    use axum::response::IntoResponse;
943    session_key.is_present();
944    tracing::trace!("callback");
945    match payload {
946        CallbackType::ToCamera(cam_arg) => {
947            // Validate YAML strings before enqueuing so callers get a proper
948            // error response instead of a silent HTTP 200 with config discarded.
949            let yaml_err: Option<String> = match &cam_arg {
950                #[cfg(feature = "flydra_feat_detect")]
951                CamArg::SetObjDetectionConfig(y) => serde_yaml::from_str::<ImPtDetectCfg>(y)
952                    .err()
953                    .map(|e| e.to_string()),
954                #[cfg(feature = "flydratrax")]
955                CamArg::CamArgSetKalmanTrackingConfig(y) => {
956                    serde_yaml::from_str::<KalmanTrackingConfig>(y)
957                        .err()
958                        .map(|e| e.to_string())
959                }
960                #[cfg(feature = "flydratrax")]
961                CamArg::CamArgSetLedProgramConfig(y) => serde_yaml::from_str::<LedProgramConfig>(y)
962                    .err()
963                    .map(|e| e.to_string()),
964                _ => None,
965            };
966            if let Some(e) = yaml_err {
967                return (
968                    StatusCode::UNPROCESSABLE_ENTITY,
969                    format!("YAML parse error: {e}"),
970                )
971                    .into_response();
972            }
973            debug!("in cb: {:?}", cam_arg);
974            app_state
975                .callback_senders
976                .cam_args_tx
977                .send(cam_arg)
978                .await
979                .ignore_send_error();
980        }
981        CallbackType::FirehoseNotify(ck) => {
982            app_state
983                .callback_senders
984                .firehose_callback_tx
985                .send(ck)
986                .await
987                .ignore_send_error();
988        }
989        CallbackType::TakeCurrentImageAsBackground => {
990            #[cfg(feature = "flydra_feat_detect")]
991            app_state
992                .callback_senders
993                .tx_frame
994                .send(Msg::TakeCurrentImageAsBackground)
995                .await
996                .ignore_send_error();
997        }
998        CallbackType::ClearBackground(value) => {
999            #[cfg(feature = "flydra_feat_detect")]
1000            app_state
1001                .callback_senders
1002                .tx_frame
1003                .send(Msg::ClearBackground(value))
1004                .await
1005                .ignore_send_error();
1006            #[cfg(not(feature = "flydra_feat_detect"))]
1007            let _ = value;
1008        }
1009        CallbackType::ToLedBox(led_box_arg) => futures::executor::block_on(async {
1010            info!("in led_box callback: {:?}", led_box_arg);
1011            app_state
1012                .callback_senders
1013                .led_box_tx_std
1014                .send(led_box_arg)
1015                .await
1016                .ignore_send_error();
1017        }),
1018    }
1019    ().into_response()
1020}
1021
1022async fn handle_auth_error(err: tower::BoxError) -> (StatusCode, &'static str) {
1023    match err.downcast::<axum_token_auth::ValidationErrors>() {
1024        Ok(err) => {
1025            tracing::error!(
1026                "Validation error(s): {:?}",
1027                err.errors().collect::<Vec<_>>()
1028            );
1029            (StatusCode::UNAUTHORIZED, "Request is not authorized")
1030        }
1031        Err(orig_err) => {
1032            tracing::error!("Unhandled internal error: {orig_err}");
1033            (StatusCode::INTERNAL_SERVER_ERROR, "internal server error")
1034        }
1035    }
1036}
1037
1038/// Information acquired from Braid when the HTTP session is established.
1039#[derive(Debug)]
1040struct BraidInfo {
1041    mainbrain_bui_loc: BuiServerAddrInfo,
1042
1043    mainbrain_session: braid_http_session::MainbrainSession,
1044    /// The address to which low-latency tracking data should be sent.
1045    ///
1046    /// Neither the IP nor the port are unspecified.
1047    camdata_udp_addr: SocketAddr,
1048    #[cfg_attr(not(feature = "flydra_feat_detect"), expect(dead_code))]
1049    tracker_cfg_src: ImPtDetectCfgSource,
1050    config_from_braid: braid_types::RemoteCameraInfoResponse,
1051}
1052
1053/// Wrapper to enforce that first message is fixed to be
1054/// [braid_types::RegisterNewCamera].
1055struct FirstMsgForced {
1056    tx: tokio::sync::mpsc::Sender<braid_types::BraidHttpApiCallback>,
1057}
1058
1059impl FirstMsgForced {
1060    fn channel(
1061        sz: usize,
1062    ) -> (
1063        Self,
1064        tokio::sync::mpsc::Receiver<braid_types::BraidHttpApiCallback>,
1065    ) {
1066        let (tx, rx) = tokio::sync::mpsc::channel(sz);
1067        (Self { tx }, rx)
1068    }
1069
1070    /// Send the first message and return the Sender.
1071    async fn send_first_msg(
1072        self,
1073        new_cam_data: braid_types::RegisterNewCamera,
1074    ) -> std::result::Result<
1075        tokio::sync::mpsc::Sender<braid_types::BraidHttpApiCallback>,
1076        tokio::sync::mpsc::error::SendError<braid_types::BraidHttpApiCallback>,
1077    > {
1078        self.tx
1079            .send(braid_types::BraidHttpApiCallback::NewCamera(new_cam_data))
1080            .await?;
1081        Ok(self.tx)
1082    }
1083}
1084
1085// -----------
1086
1087/// top-level function once args are parsed from CLI.
1088pub fn run_strand_cam_app<M, C, G>(
1089    mymod: ci2_async::ThreadedAsyncCameraModule<M, C, G>,
1090    args: StrandCamArgs,
1091    app_name: &'static str,
1092) -> Result<ci2_async::ThreadedAsyncCameraModule<M, C, G>>
1093where
1094    M: ci2::CameraModule<CameraType = C, Guard = G> + 'static,
1095    C: 'static + ci2::Camera + Send,
1096    G: Send + 'static,
1097{
1098    let (log_dir, data_dir) = if let Some(data_dir) = &args.data_dir {
1099        (data_dir.clone(), data_dir.clone())
1100    } else {
1101        (
1102            // default log_dir is home.
1103            home::home_dir().ok_or_else(|| {
1104                eyre::eyre!("Could not determine home directory and data directory not set.")
1105            })?,
1106            // default data_dir is pwd.
1107            PathBuf::from("."),
1108        )
1109    };
1110
1111    // Initial log file name has process ID in case multiple cameras are
1112    // launched simultaneously. The (still open) log file gets renamed later to
1113    // include the camera name. We need to start logging as soon as possible
1114    // (before we necessarily know the camera name) because we may need to debug
1115    // connectivity problems to Braid or problems starting the camera.
1116    let log_file_time = chrono::Local::now();
1117    let initial_log_file_name = log_file_time
1118        .format(".strand-cam-%Y%m%d_%H%M%S.%f")
1119        .to_string()
1120        + &format!("-{}.log", std::process::id());
1121    let initial_log_file_name = log_dir.join(&initial_log_file_name);
1122    // TODO: delete log files older than, e.g. one week.
1123
1124    #[cfg(feature = "eframe-gui")]
1125    let disable_console = true;
1126
1127    #[cfg(not(feature = "eframe-gui"))]
1128    let disable_console = args.disable_console;
1129
1130    let initial_log_file_name2 = initial_log_file_name.clone();
1131
1132    let _guard =
1133        env_tracing_logger::initiate_logging(Some(&initial_log_file_name2), disable_console)
1134            .map_err(|e| eyre!("error initiating logging: {e}"))?;
1135
1136    // create tokio runtime
1137    let runtime = tokio::runtime::Builder::new_multi_thread()
1138        .enable_all()
1139        .worker_threads(4)
1140        .thread_name("strand-cam-runtime")
1141        .thread_stack_size(3 * 1024 * 1024)
1142        .build()?;
1143
1144    let log_file_info = LogFileInfo {
1145        initial_log_file_name,
1146        log_dir,
1147        data_dir,
1148        log_file_time,
1149    };
1150
1151    #[cfg(feature = "eframe-gui")]
1152    {
1153        let (quit_tx, quit_rx) = tokio::sync::mpsc::channel(1);
1154
1155        let gui_singleton = Arc::new(std::sync::Mutex::new(GuiShared::default()));
1156        let gui_singleton2 = gui_singleton.clone();
1157
1158        let (frame_tx, frame_rx) = tokio::sync::watch::channel(Arc::new(
1159            strand_dynamic_frame::DynamicFrameOwned::from_static(
1160                formats::owned::OImage::<formats::pixel_format::Mono8>::zeros(0, 0, 0).unwrap(),
1161            ),
1162        ));
1163        let (egui_ctx_tx, egui_ctx_rx) = std::sync::mpsc::channel();
1164
1165        let gui_app_stuff = Some(GuiAppStuff {
1166            quit_rx,
1167            frame_tx,
1168            egui_ctx_rx,
1169        });
1170
1171        // Move tokio runtime to new thread to keep GUI event loop on initial thread.
1172        let tokio_thread_jh = std::thread::Builder::new()
1173            .name("tokio-thread".to_string())
1174            .spawn(move || {
1175                let mymod = runtime.block_on(run_after_maybe_connecting_to_braid(
1176                    mymod,
1177                    args,
1178                    app_name,
1179                    log_file_info,
1180                    gui_app_stuff,
1181                    gui_singleton2,
1182                ))?;
1183
1184                info!("done");
1185                Ok(mymod)
1186            })
1187            .map_err(|e| eyre::anyhow!("runtime failed with error {e}"))?;
1188
1189        let native_options = Default::default();
1190
1191        eframe::run_native(
1192            "Strand Camera",
1193            native_options,
1194            Box::new(move |cc| {
1195                Ok(Box::new(gui_app::StrandCamEguiApp::new(
1196                    quit_tx,
1197                    cc,
1198                    gui_singleton,
1199                    frame_rx,
1200                    egui_ctx_tx,
1201                )))
1202            }),
1203        )
1204        .map_err(|e| eyre::anyhow!("running failed with error {e}"))?;
1205
1206        // Block until tokio done.
1207        tokio_thread_jh.join().unwrap()
1208    }
1209
1210    #[cfg(not(feature = "eframe-gui"))]
1211    {
1212        let gui_singleton = ();
1213
1214        let mymod = runtime.block_on(run_after_maybe_connecting_to_braid(
1215            mymod,
1216            args,
1217            app_name,
1218            log_file_info,
1219            None,
1220            gui_singleton,
1221        ))?;
1222
1223        info!("done");
1224        Ok(mymod)
1225    }
1226}
1227
1228struct GuiAppStuff {
1229    quit_rx: tokio::sync::mpsc::Receiver<()>,
1230    #[cfg(feature = "eframe-gui")]
1231    frame_tx: tokio::sync::watch::Sender<gui_app::ImType>,
1232    #[cfg(feature = "eframe-gui")]
1233    egui_ctx_rx: std::sync::mpsc::Receiver<eframe::egui::Context>,
1234}
1235
1236/// Connect to the braid server and return information.
1237///
1238/// Store cookie if set by braid so that next connection does not need token.
1239async fn connect_to_braid(braid_args: &BraidArgs) -> Result<BraidInfo> {
1240    info!("Will connect to braid at \"{}\"", braid_args.braid_url);
1241    let mainbrain_bui_loc = BuiServerAddrInfo::parse_url_with_token(&braid_args.braid_url)?;
1242
1243    let jar: cookie_store::CookieStore = match Preferences::load(&APP_INFO, BRAID_COOKIE_KEY) {
1244        Ok(jar) => {
1245            tracing::debug!("loaded cookie store {BRAID_COOKIE_KEY}");
1246            jar
1247        }
1248        Err(e) => {
1249            tracing::debug!("cookie store {BRAID_COOKIE_KEY} not loaded: {e} {e:?}");
1250            cookie_store::CookieStore::new(None)
1251        }
1252    };
1253    let jar = Arc::new(RwLock::new(jar));
1254    let mut mainbrain_session =
1255        braid_http_session::create_mainbrain_session(mainbrain_bui_loc.clone(), jar.clone())
1256            .await
1257            .with_context(|| format!("While connecting to Braid at {}", braid_args.braid_url))?;
1258    tracing::debug!("Opened HTTP session with Braid.");
1259    {
1260        // We have the cookie from braid now, so store it to disk.
1261        let jar = jar.read().unwrap();
1262        Preferences::save(&*jar, &APP_INFO, BRAID_COOKIE_KEY)?;
1263        // The jar holds live session cookies; keep its file owner-only.
1264        braid_types::harden_prefs_file(&APP_INFO, BRAID_COOKIE_KEY);
1265        tracing::debug!("saved cookie store {BRAID_COOKIE_KEY}");
1266    }
1267
1268    let camera_name = braid_types::RawCamName::new(braid_args.camera_name.clone());
1269
1270    let config_from_braid: braid_types::RemoteCameraInfoResponse =
1271        mainbrain_session.get_remote_info(&camera_name).await?;
1272
1273    let camdata_udp_ip = mainbrain_bui_loc.addr().ip();
1274    let camdata_udp_port = config_from_braid.camdata_udp_port;
1275    let camdata_udp_addr = SocketAddr::new(camdata_udp_ip, camdata_udp_port);
1276
1277    let tracker_cfg_src = crate::ImPtDetectCfgSource::ChangesNotSavedToDisk(
1278        config_from_braid.config.point_detection_config.clone(),
1279    );
1280
1281    Ok(BraidInfo {
1282        mainbrain_bui_loc,
1283        mainbrain_session,
1284        config_from_braid,
1285        camdata_udp_addr,
1286        tracker_cfg_src,
1287    })
1288}
1289
1290struct LogFileInfo {
1291    initial_log_file_name: PathBuf,
1292    /// where log files are saved
1293    log_dir: PathBuf,
1294    /// where movies are saved
1295    data_dir: PathBuf,
1296    log_file_time: chrono::DateTime<chrono::Local>,
1297}
1298
1299/// First, connect to Braid if requested, then run.
1300async fn run_after_maybe_connecting_to_braid<M, C, G>(
1301    mymod: ci2_async::ThreadedAsyncCameraModule<M, C, G>,
1302    args: StrandCamArgs,
1303    app_name: &'static str,
1304    log_file_info: LogFileInfo,
1305    gui_app_stuff: Option<GuiAppStuff>,
1306    gui_singleton: ArcMutGuiSingleton,
1307) -> Result<ci2_async::ThreadedAsyncCameraModule<M, C, G>>
1308where
1309    M: ci2::CameraModule<CameraType = C, Guard = G>,
1310    C: 'static + ci2::Camera + Send,
1311    G: Send,
1312{
1313    let cfg_from_braid;
1314    let strand_cam_bui_http_address_string = match &args.standalone_or_braid {
1315        StandaloneOrBraid::Braid(braid_args) => {
1316            // Connect to braid and get configuration if running in braid context.
1317            let from_mainbrain = connect_to_braid(braid_args).await?;
1318
1319            // We have already connected to the Mainbrain BUI server and gotten
1320            // configuration information from it. Use that to set things up.
1321            let http_server_addr = from_mainbrain
1322                .config_from_braid
1323                .config
1324                .http_server_addr
1325                .clone();
1326            let mainbrain_bui_loc = &from_mainbrain.mainbrain_bui_loc;
1327
1328            let strand_cam_bui_http_address_string = braid_strand_cam_http_address(
1329                mainbrain_bui_loc.addr().ip(),
1330                http_server_addr,
1331                find_local_ip_for_remote,
1332            );
1333            cfg_from_braid = Some(from_mainbrain);
1334            strand_cam_bui_http_address_string
1335        }
1336        StandaloneOrBraid::Standalone(standalone_args) => {
1337            cfg_from_braid = None;
1338            // The best way I've found to set the default value because we need
1339            // to keep `http_server_addr` None when calling from Braid. If we
1340            // change the default, we also need to change the docs and
1341            // docstrings.
1342            const BRAID_HTTP_ADDR: &str = "127.0.0.1:3440";
1343            standalone_args
1344                .http_server_addr
1345                .clone()
1346                .unwrap_or_else(|| BRAID_HTTP_ADDR.to_string())
1347        }
1348    };
1349    tracing::debug!("Strand Camera HTTP server: {strand_cam_bui_http_address_string}");
1350
1351    let target_feature_string = target::features().join(", ");
1352    info!("Compiled with features: {}", target_feature_string);
1353
1354    let requested_camera_name = match &args.standalone_or_braid {
1355        StandaloneOrBraid::Standalone(args) => args.camera_name.clone(),
1356        StandaloneOrBraid::Braid(args) => Some(args.camera_name.clone()),
1357    };
1358
1359    debug!("Request for camera \"{requested_camera_name:?}\"");
1360
1361    // -----------------------------------------------
1362
1363    info!("camera module: {}", mymod.name());
1364
1365    let cam_infos = mymod.camera_infos()?;
1366    if cam_infos.is_empty() {
1367        eyre::bail!("No cameras found.");
1368    }
1369
1370    for cam_info in cam_infos.iter() {
1371        info!("  camera {:?} detected", cam_info.name());
1372    }
1373
1374    let use_camera_name = match requested_camera_name {
1375        Some(ref name) => name,
1376        None => cam_infos[0].name(),
1377    };
1378
1379    // Rename the log file (which is open and being written to) so that the name
1380    // includes the camera name.
1381    let new_log_file_name = log_file_info
1382        .log_file_time
1383        .format(".strand-cam-%Y%m%d_%H%M%S.%f")
1384        .to_string()
1385        + &format!("-{}.log", use_camera_name);
1386    let new_log_file_name = log_file_info.log_dir.join(&new_log_file_name);
1387
1388    tracing::debug!(
1389        "Renaming log file \"{}\" -> \"{}\"",
1390        log_file_info.initial_log_file_name.display(),
1391        new_log_file_name.display()
1392    );
1393    std::fs::rename(&log_file_info.initial_log_file_name, &new_log_file_name).with_context(
1394        || {
1395            format!(
1396                "Renaming log file \"{}\" -> \"{}\"",
1397                log_file_info.initial_log_file_name.display(),
1398                new_log_file_name.display()
1399            )
1400        },
1401    )?;
1402
1403    run(
1404        mymod,
1405        args,
1406        app_name,
1407        cfg_from_braid,
1408        strand_cam_bui_http_address_string,
1409        use_camera_name,
1410        gui_app_stuff,
1411        gui_singleton,
1412        log_file_info.data_dir,
1413    )
1414    .await
1415}
1416
1417// -----------
1418
1419/// Forward messages from elsewhere in Strand Cam to mainbrain.
1420///
1421/// The future runs until the trainsmit_msg_rx channel is closed otherwise ends
1422/// only on error.
1423async fn forward_to_mainbrain(
1424    mut transmit_msg_rx: tokio::sync::mpsc::Receiver<braid_types::BraidHttpApiCallback>,
1425    mut mainbrain_session: braid_http_session::MainbrainSession,
1426) -> Result<()> {
1427    while let Some(msg) = transmit_msg_rx.recv().await {
1428        // We have a message from elsewhere in Strand Cam to send to mainbrain.
1429        mainbrain_session
1430            .post_callback_message(msg)
1431            .await
1432            .context("failed sending message to mainbrain")?;
1433    }
1434    Ok(())
1435}
1436
1437// -----------
1438
1439/// This is the main function where we spend all time after parsing startup args
1440/// and, in case of connecting to braid, getting the inital connection
1441/// information.
1442///
1443/// This function is way too huge and should be refactored.
1444#[tracing::instrument(skip(
1445    mymod,
1446    args,
1447    app_name,
1448    braid_info,
1449    strand_cam_bui_http_address_string,
1450    gui_app_stuff,
1451    gui_singleton,
1452    data_dir
1453))]
1454#[expect(
1455    clippy::too_many_arguments,
1456    reason = "oh this is ugly. refactor at some point."
1457)]
1458async fn run<M, C, G>(
1459    mut mymod: ci2_async::ThreadedAsyncCameraModule<M, C, G>,
1460    args: StrandCamArgs,
1461    app_name: &'static str,
1462    braid_info: Option<BraidInfo>,
1463    strand_cam_bui_http_address_string: String,
1464    cam: &str,
1465    gui_app_stuff: Option<GuiAppStuff>,
1466    gui_singleton: ArcMutGuiSingleton,
1467    data_dir: PathBuf,
1468) -> Result<ci2_async::ThreadedAsyncCameraModule<M, C, G>>
1469where
1470    M: ci2::CameraModule<CameraType = C, Guard = G>,
1471    C: 'static + ci2::Camera + Send,
1472    G: Send,
1473{
1474    let use_camera_name = cam; // simple arg name important for tracing::instrument
1475    let settings_file_ext = mymod.settings_file_extension().to_string();
1476
1477    let (quit_rx, gui_stuff2) = if let Some(gas) = gui_app_stuff {
1478        let quit_rx = gas.quit_rx;
1479
1480        #[cfg(feature = "eframe-gui")]
1481        let gui_stuff2 = {
1482            let frame_tx = gas.frame_tx;
1483            let egui_ctx_rx = gas.egui_ctx_rx;
1484
1485            // Wait for egui context.
1486            let egui_ctx: eframe::egui::Context = egui_ctx_rx.recv().unwrap();
1487
1488            Some((frame_tx, egui_ctx))
1489        };
1490
1491        #[cfg(not(feature = "eframe-gui"))]
1492        let gui_stuff2: Option<()> = None;
1493
1494        (Some(quit_rx), gui_stuff2)
1495    } else {
1496        (None, None)
1497    };
1498
1499    let mut cam = match mymod.threaded_async_camera(use_camera_name) {
1500        Ok(cam) => cam,
1501        Err(e) => {
1502            let msg = format!("{e}");
1503            error!("{}", msg);
1504            return Err(e.into());
1505        }
1506    };
1507
1508    let raw_name = cam.name().to_string();
1509    info!("  got camera {}", raw_name);
1510    let raw_cam_name = RawCamName::new(raw_name);
1511
1512    let camera_gamma = cam
1513        .feature_float("Gamma")
1514        .map_err(|e| warn!("Ignoring error getting gamma: {}", e))
1515        .ok()
1516        .map(|x: f64| x as f32);
1517
1518    // Use `Result` as an enum with two options. It's not the case that one is a
1519    // non-error and the other an error condition. We just use `Result` rather
1520    // than defining our own enum type here.
1521    let res_braid = match (&braid_info, &args.standalone_or_braid) {
1522        (Some(bi), StandaloneOrBraid::Braid(_)) => Ok(bi),
1523        (None, StandaloneOrBraid::Standalone(a)) => Err(a),
1524        (Some(_), StandaloneOrBraid::Standalone(_)) | (None, StandaloneOrBraid::Braid(_)) => {
1525            unreachable!()
1526        }
1527    };
1528
1529    let camera_settings_filename = match &res_braid {
1530        Ok(bi) => bi.config_from_braid.config.camera_settings_filename.clone(),
1531        Err(a) => a.camera_settings_filename.clone(),
1532    };
1533
1534    let pixel_format = match &res_braid {
1535        Ok(bi) => bi.config_from_braid.config.pixel_format.clone(),
1536        Err(a) => a.pixel_format.clone(),
1537    };
1538
1539    let send_image_to_braid_interval = res_braid.as_ref().ok().map(|bi| {
1540        std::time::Duration::from_millis(
1541            bi.config_from_braid.config.send_current_image_interval_msec,
1542        )
1543    });
1544
1545    let acquisition_duration_allowed_imprecision_msec = match &res_braid {
1546        Ok(bi) => {
1547            bi.config_from_braid
1548                .config
1549                .acquisition_duration_allowed_imprecision_msec
1550        }
1551        Err(a) => a.acquisition_duration_allowed_imprecision_msec,
1552    };
1553    #[cfg(not(feature = "flydra_feat_detect"))]
1554    let _ = acquisition_duration_allowed_imprecision_msec;
1555
1556    let (frame_rate_limit_supported, mut frame_rate_limit_enabled) = if let Some(fname) =
1557        &camera_settings_filename
1558    {
1559        let settings = std::fs::read_to_string(fname).with_context(|| {
1560            format!(
1561                "Failed to read camera settings from file \"{}\"",
1562                fname.display()
1563            )
1564        })?;
1565
1566        cam.node_map_load(&settings)?;
1567        info!("loaded camera settings file \"{}\"", fname.display());
1568        (false, false)
1569    } else {
1570        for pixfmt in cam.possible_pixel_formats()?.iter() {
1571            debug!("  possible pixel format: {}", pixfmt);
1572        }
1573
1574        if let Some(ref pixfmt_str) = pixel_format {
1575            use std::str::FromStr;
1576            let pixfmt = PixFmt::from_str(pixfmt_str).map_err(|e: &str| eyre!(e.to_string()))?;
1577            info!("  setting pixel format: {}", pixfmt);
1578            cam.set_pixel_format(pixfmt)?;
1579        }
1580
1581        debug!("  current pixel format: {}", cam.pixel_format()?);
1582
1583        let (frame_rate_limit_supported, frame_rate_limit_enabled) = {
1584            // This entire section should be removed and converted to a query
1585            // of the cameras capabilities.
1586
1587            // Save the value of whether the frame rate limiter is enabled.
1588            let frame_rate_limit_enabled = feature_or(cam.acquisition_frame_rate_enable(), false)?;
1589            debug!("frame_rate_limit_enabled {}", frame_rate_limit_enabled);
1590
1591            // Check if we can set the frame rate, first by setting a limit to be on.
1592            let frame_rate_limit_supported = match cam.set_acquisition_frame_rate_enable(true) {
1593                Ok(()) => {
1594                    debug!("set set_acquisition_frame_rate_enable true");
1595                    // Then by setting a limit to be off.
1596                    match cam.set_acquisition_frame_rate_enable(false) {
1597                        Ok(()) => {
1598                            debug!("{}:{}", file!(), line!());
1599                            true
1600                        }
1601                        Err(e) => {
1602                            debug!("err {} {}:{}", e, file!(), line!());
1603                            false
1604                        }
1605                    }
1606                }
1607                Err(e) => {
1608                    debug!("err {} {}:{}", e, file!(), line!());
1609                    false
1610                }
1611            };
1612
1613            if frame_rate_limit_supported {
1614                // Restore the state of the frame rate limiter.
1615                cam.set_acquisition_frame_rate_enable(frame_rate_limit_enabled)?;
1616                debug!("set frame_rate_limit_enabled {}", frame_rate_limit_enabled);
1617            }
1618
1619            (frame_rate_limit_supported, frame_rate_limit_enabled)
1620        };
1621
1622        match cam.feature_enum_set("AcquisitionMode", "Continuous") {
1623            Ok(()) => {}
1624            Err(e) => {
1625                debug!("Ignoring error when setting AcquisitionMode: {}", e);
1626            }
1627        }
1628        (frame_rate_limit_supported, frame_rate_limit_enabled)
1629    };
1630
1631    let settings_on_start = feature_or(cam.node_map_save(), String::new())?;
1632
1633    let res_braid = match (&braid_info, &args.standalone_or_braid) {
1634        (Some(bi), StandaloneOrBraid::Braid(_)) => Ok(bi),
1635        (None, StandaloneOrBraid::Standalone(a)) => Err(a),
1636        (Some(_), StandaloneOrBraid::Standalone(_)) | (None, StandaloneOrBraid::Braid(_)) => {
1637            unreachable!()
1638        }
1639    };
1640
1641    let force_camera_sync_mode = match &res_braid {
1642        Ok(bi) => bi.config_from_braid.force_camera_sync_mode,
1643        Err(a) => a.force_camera_sync_mode,
1644    };
1645
1646    let camdata_udp_addr = match &res_braid {
1647        Ok(bi) => Some(bi.camdata_udp_addr),
1648        Err(_a) => None,
1649    };
1650
1651    let software_limit_framerate = match &res_braid {
1652        Ok(bi) => bi.config_from_braid.software_limit_framerate.clone(),
1653        Err(a) => a.software_limit_framerate.clone(),
1654    };
1655
1656    #[cfg(feature = "flydra_feat_detect")]
1657    let tracker_cfg_src = match &res_braid {
1658        Ok(bi) => bi.tracker_cfg_src.clone(),
1659        Err(a) => a.tracker_cfg_src.clone(),
1660    };
1661
1662    // Here we just create some default, it does not matter what, because it
1663    // will not be used for anything.
1664    #[cfg(not(feature = "flydra_feat_detect"))]
1665    let im_pt_detect_cfg = flydra_pt_detect_cfg::default_absdiff();
1666
1667    #[cfg(feature = "flydra_feat_detect")]
1668    let im_pt_detect_cfg = match &tracker_cfg_src {
1669        ImPtDetectCfgSource::ChangedSavedToDisk(src) => {
1670            // Retrieve the saved preferences
1671            let (app_info, prefs_key) = src;
1672            match ImPtDetectCfg::load(app_info, prefs_key) {
1673                Ok(cfg) => cfg,
1674                Err(e) => {
1675                    info!(
1676                        "Failed loading image detection config ({}), using defaults.",
1677                        e
1678                    );
1679                    default_im_pt_detect()
1680                }
1681            }
1682        }
1683        ImPtDetectCfgSource::ChangesNotSavedToDisk(cfg) => cfg.clone(),
1684    };
1685
1686    let (mainbrain_session, trigger_type) = match braid_info {
1687        Some(bi) => (
1688            Some(bi.mainbrain_session),
1689            Some(bi.config_from_braid.trig_config),
1690        ),
1691        None => (None, None),
1692    };
1693
1694    // Setup PTP and let clocks converge prior to starting acquisition.
1695    if let Some(TriggerType::PtpSync(ptpcfg)) = &trigger_type {
1696        let mut clock_sync_threshold_usecs = None;
1697        if let Some(period_usec) = ptpcfg.periodic_signal_period_usec {
1698            let period_usec_int = period_usec as i64;
1699            if period_usec - period_usec_int as f64 > 1.0 {
1700                eyre::bail!("period cannot be specified to sub-microsecond precision");
1701            }
1702            clock_sync_threshold_usecs = Some(period_usec_int / 2);
1703            if cam.feature_float(PERIOD_NAME)? != period_usec {
1704                cam.feature_float_set(PERIOD_NAME, period_usec)?;
1705                tracing::debug!(
1706                    "Set camera parameter {PERIOD_NAME} to {period_usec} microseconds."
1707                );
1708            }
1709        };
1710        if !cam.feature_bool("PtpEnable")? {
1711            tracing::debug!("Enabling PTP.");
1712            cam.feature_bool_set("PtpEnable", true)?;
1713        }
1714        // If period not set, default to 1 millisecond.
1715        let clock_sync_threshold_nanos = clock_sync_threshold_usecs.unwrap_or(1_000) * 1_000;
1716        loop {
1717            cam.command_execute("PtpDataSetLatch", true)?;
1718            let ptp_offset_from_master = cam.feature_int("PtpOffsetFromMaster")?;
1719            // Basler docs: "PtpOffsetFromMaster: Indicates the estimated
1720            // temporal offset between the master clock and the clock of the
1721            // current PTP device in ticks (1 tick = 1 nanosecond)."
1722            tracing::debug!("PTP clock offset {ptp_offset_from_master} nanoseconds.");
1723            if ptp_offset_from_master.abs() < clock_sync_threshold_nanos {
1724                // if within threshold from master, call it good enough.
1725                break;
1726            }
1727            tracing::info!(
1728                "PTP clock offset {ptp_offset_from_master} nanoseconds (threshold \
1729                        {clock_sync_threshold_nanos}), waiting 1 second for convergence."
1730            );
1731            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
1732        }
1733        tracing::info!(
1734            "PTP clock within threshold {clock_sync_threshold_nanos} nanoseconds from master."
1735        );
1736
1737        if cam.feature_enum("TriggerMode")? != "On" {
1738            cam.feature_enum_set("TriggerMode", "On")?;
1739        }
1740        if cam.feature_enum("TriggerSource")? != "PeriodicSignal1" {
1741            cam.feature_enum_set("TriggerSource", "PeriodicSignal1")?;
1742        }
1743    };
1744
1745    // Start the camera.
1746    cam.acquisition_start()?;
1747    // Buffer 20 frames to be processed before dropping them.
1748    let (tx_frame, rx_frame) = tokio::sync::mpsc::channel::<Msg>(20);
1749    let tx_frame2 = tx_frame.clone();
1750
1751    // Get initial frame to determine width, height and pixel_format.
1752    debug!("  started acquisition, waiting for first frame");
1753    let frame = cam.next_frame()?;
1754    info!(
1755        "  acquired first frame: {}x{}",
1756        frame.width(),
1757        frame.height()
1758    );
1759
1760    #[cfg(target_os = "linux")]
1761    let v4l_out_stream = {
1762        let frame_image = frame.image.borrow();
1763        use machine_vision_formats::Stride;
1764        if let Some(v4l_device) = &args.v4l2loopback {
1765            use machine_vision_formats::ImageData;
1766            let frame = frame_image
1767                .as_static::<formats::pixel_format::Mono8>()
1768                .ok_or_else(|| {
1769                    eyre::eyre!(
1770                        "Currently unsupported pixel format for v4l2loopback: {:?}",
1771                        frame.pixel_format()
1772                    )
1773                })?;
1774            tracing::info!("Using v4l2loopback device {}", v4l_device.display());
1775            let out = v4l::device::Device::with_path(v4l_device).with_context(|| {
1776                format!("opening V4L2 loopback device {}", v4l_device.display())
1777            })?;
1778            let source_fmt = v4l::format::Format {
1779                width: frame.width(),
1780                height: frame.height(),
1781                stride: frame.stride().try_into()?,
1782                field_order: v4l::format::field::FieldOrder::Progressive,
1783                flags: 0.into(),
1784                size: u32::try_from(frame.stride())? * frame.height(),
1785                quantization: v4l::format::quantization::Quantization::FullRange,
1786                transfer: v4l::format::transfer::TransferFunction::None,
1787                fourcc: v4l::format::fourcc::FourCC::new(b"GREY"),
1788                colorspace: v4l::format::colorspace::Colorspace::RAW,
1789            };
1790            tracing::info!("Setting v4l2loopback format: {:?}", source_fmt);
1791            v4l::video::Output::set_format(&out, &source_fmt)?;
1792
1793            let mut v4l_out_stream =
1794                v4l::io::mmap::stream::Stream::new(&out, v4l::buffer::Type::VideoOutput)?;
1795
1796            let (buf_out, buf_out_meta) = v4l::io::traits::OutputStream::next(&mut v4l_out_stream)?;
1797            let buf_in = frame.image_data();
1798            let bytesused = buf_in.len().try_into()?;
1799
1800            let buf_out = &mut buf_out[0..buf_in.len()];
1801            buf_out.copy_from_slice(buf_in);
1802            buf_out_meta.field = 0;
1803            buf_out_meta.bytesused = bytesused;
1804            Some(v4l_out_stream)
1805        } else {
1806            None
1807        }
1808    };
1809
1810    let (firehose_tx, firehose_rx) = tokio::sync::mpsc::channel::<AnnotatedFrame>(5);
1811
1812    // Put first frame in channel.
1813    firehose_tx
1814        .send(AnnotatedFrame {
1815            frame: frame.image.clone(),
1816            found_points: vec![],
1817            valid_display: None,
1818            annotations: vec![],
1819        })
1820        .await
1821        .unwrap();
1822    // .map_err(|e| anhow::anyhow!("failed to send frame"))?;
1823
1824    let image_width = frame.width();
1825    let image_height = frame.height();
1826
1827    let current_image_png = frame
1828        .image
1829        .borrow()
1830        .to_encoded_buffer(convert_image::EncoderOptions::Png)?;
1831
1832    // If we have a session with mainbrain, create a channel that collects
1833    // messages from within Strand Cam to forward to mainbrain and create the
1834    // forwarding future. If we don't have a session, just create a dummy future
1835    // that never resolves.
1836    let (first_msg_tx, forward_to_mainbrain_fut): (_, Pin<Box<dyn Future<Output = _>>>) = {
1837        if let Some(mainbrain_session) = mainbrain_session {
1838            let (tx, rx) = FirstMsgForced::channel(10);
1839            let fut = forward_to_mainbrain(rx, mainbrain_session);
1840            (Some(tx), Box::pin(fut))
1841        } else {
1842            (None, Box::pin(std::future::pending()))
1843        }
1844    };
1845
1846    const PERIOD_NAME: &str = "BslPeriodicSignalPeriod";
1847
1848    let mut local_remote = Vec::new();
1849    let mut local_time0 = None;
1850    let mut cam_time0 = None;
1851    let mut device_clock_model = None;
1852
1853    if trigger_type == Some(TriggerType::DeviceTimestamp) {
1854        // Attempt to relate camera timestamps to our clock
1855        tracing::info!("Reading camera timestamps to fit initial clock model.");
1856
1857        let n_pts = 5;
1858        let mut tmp_debug_device_timestamp = None;
1859        for i in 0..n_pts {
1860            let (local, cam_time) = measure_times(&cam)?;
1861            tmp_debug_device_timestamp.get_or_insert(cam_time);
1862            let local_time_nanos = braid_types::PtpStamp::try_from(local).unwrap().get();
1863            local_time0.get_or_insert(local_time_nanos);
1864            let cam_time_ts = braid_types::PtpStamp::new(cam_time.try_into().unwrap()).get();
1865            cam_time0.get_or_insert(cam_time_ts);
1866
1867            let this_local_time0 = local_time0.as_ref().unwrap();
1868            let this_cam_time0 = cam_time0.as_ref().unwrap();
1869            // dbg!(&local_time_nanos);
1870            // dbg!(&local_time_secs);
1871            let local_elapsed_nanos = local_time_nanos - this_local_time0;
1872            let device_elapsed_nanos = cam_time_ts - this_cam_time0;
1873            local_remote.push((device_elapsed_nanos as f64, local_elapsed_nanos as f64));
1874            // local_remote.push((cam_time_ts as f64, local_ts as f64));
1875            if i < n_pts - 1 {
1876                tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
1877            }
1878        }
1879        let (gain, offset, residuals) = clock_model::fit_time_model(&local_remote)?;
1880        dbg!((gain, offset, residuals));
1881
1882        let cm = strand_cam_bui_types::ClockModel {
1883            gain,
1884            offset,
1885            residuals,
1886            n_measurements: local_remote.len().try_into().unwrap(),
1887        };
1888
1889        let device_timestamp: u64 = tmp_debug_device_timestamp.unwrap().try_into().unwrap();
1890        let this_cam_time0 = cam_time0.as_ref().unwrap();
1891        let device_elapsed_nanos = device_timestamp - this_cam_time0;
1892        let local_estimate_elapsed_nanos: f64 = (device_elapsed_nanos as f64) * cm.gain + cm.offset;
1893
1894        dbg!((local_estimate_elapsed_nanos, device_timestamp, &cm));
1895        device_clock_model = Some(cm);
1896    }
1897
1898    let local_and_cam_time0 = if let Some(ct0) = cam_time0 {
1899        let local_time0 = local_time0.as_ref().unwrap();
1900        Some((*local_time0, ct0))
1901    } else {
1902        None
1903    };
1904
1905    let camera_periodic_signal_period_usec = {
1906        match cam.feature_float(PERIOD_NAME) {
1907            Ok(value) => {
1908                tracing::debug!("Camera parameter {PERIOD_NAME}: {value} microseconds");
1909                Some(value)
1910            }
1911            Err(e) => {
1912                tracing::debug!("Could not read feature {PERIOD_NAME}: {e}");
1913                None
1914            }
1915        }
1916    };
1917
1918    let (cam_args_tx, cam_args_rx) = tokio::sync::mpsc::channel(100);
1919    let (led_box_tx_std, led_box_rx) = tokio::sync::mpsc::channel(20);
1920
1921    let led_box_heartbeat_update_arc = Arc::new(RwLock::new(None));
1922
1923    let (gain_min, gain_max) = feature_or(cam.gain_range(), (0.0, 0.0))?;
1924    let gain_ranged = RangedValue {
1925        name: "gain".into(),
1926        unit: "dB".into(),
1927        min: gain_min,
1928        max: gain_max,
1929        current: feature_or(cam.gain(), 0.0)?,
1930    };
1931    let (exposure_min, exposure_max) = feature_or(cam.exposure_time_range(), (0.0, 0.0))?;
1932    let exposure_ranged = RangedValue {
1933        name: "exposure time".into(),
1934        unit: "μsec".into(),
1935        min: exposure_min,
1936        max: exposure_max,
1937        current: feature_or(cam.exposure_time(), 0.0)?,
1938    };
1939    let gain_auto = cam.gain_auto().ok();
1940    let exposure_auto = cam.exposure_auto().ok();
1941
1942    let mut frame_rate_limit = if frame_rate_limit_supported {
1943        let (min, max) = cam.acquisition_frame_rate_range()?;
1944        Some(RangedValue {
1945            name: "frame rate".into(),
1946            unit: "Hz".into(),
1947            min,
1948            max,
1949            current: cam.acquisition_frame_rate()?,
1950        })
1951    } else {
1952        None
1953    };
1954
1955    let current_cam_settings_extension = settings_file_ext.to_string();
1956
1957    // Load the persistent secret once: it both mints the self-expiring access
1958    // token in `start_listener` and validates it in the auth layer below.
1959    let persistent_secret = http_router::load_persistent_secret(args.secret.clone())?;
1960
1961    let (listener, http_camserver_info) =
1962        braid_types::start_listener(&strand_cam_bui_http_address_string, &persistent_secret)
1963            .await?;
1964    let listen_addr = listener.local_addr()?;
1965
1966    let mut transmit_msg_tx = None;
1967    if let Some(first_msg_tx) = first_msg_tx {
1968        let new_cam_data = braid_types::RegisterNewCamera {
1969            raw_cam_name: raw_cam_name.clone(),
1970            http_camserver_info: Some(BuiServerInfo::Server(http_camserver_info.clone())),
1971            cam_settings_data: Some(braid_types::UpdateCamSettings {
1972                current_cam_settings_buf: settings_on_start,
1973                current_cam_settings_extension: settings_file_ext,
1974            }),
1975            current_image_png: current_image_png.into(),
1976            camera_periodic_signal_period_usec,
1977        };
1978
1979        // Get the generic sender back.
1980        transmit_msg_tx = Some(first_msg_tx.send_first_msg(new_cam_data).await?);
1981        tracing::info!("Registered camera with Braid.");
1982    }
1983
1984    if force_camera_sync_mode {
1985        cam.start_default_external_triggering().unwrap();
1986        if let Some(transmit_msg_tx) = &transmit_msg_tx {
1987            send_cam_settings_to_braid(
1988                &cam.node_map_save()?,
1989                transmit_msg_tx,
1990                &current_cam_settings_extension,
1991                &raw_cam_name,
1992            )
1993            .await?;
1994        }
1995    }
1996
1997    if camera_settings_filename.is_none()
1998        && let StartSoftwareFrameRateLimit::Enable(fps_limit) = &software_limit_framerate
1999    {
2000        // Set the camera.
2001        cam.set_software_frame_rate_limit(*fps_limit).unwrap();
2002        // Store the values we set.
2003        if let Some(ref mut ranged) = frame_rate_limit {
2004            ranged.current = cam.acquisition_frame_rate()?;
2005        } else {
2006            panic!("cannot set software frame rate limit");
2007        }
2008        frame_rate_limit_enabled = cam.acquisition_frame_rate_enable()?;
2009    }
2010
2011    let trigger_mode = feature_or(cam.trigger_mode(), ci2::TriggerMode::Off)?;
2012    let trigger_selector = feature_or(cam.trigger_selector(), ci2::TriggerSelector::FrameStart)?;
2013    debug!("  got camera values");
2014
2015    #[cfg(feature = "flydra_feat_detect")]
2016    let camera_cfg = CameraCfgFview2_0_26 {
2017        vendor: cam.vendor().into(),
2018        model: cam.model().into(),
2019        serial: cam.serial().into(),
2020        width: cam.width()?,
2021        height: cam.height()?,
2022    };
2023
2024    #[cfg(feature = "flydratrax")]
2025    let kalman_tracking_config = {
2026        if let ImPtDetectCfgSource::ChangedSavedToDisk(ref src) = tracker_cfg_src {
2027            // Retrieve the saved preferences
2028            let (app_info, _im_pt_detect_prefs_key) = src;
2029            match KalmanTrackingConfig::load(app_info, KALMAN_TRACKING_PREFS_KEY) {
2030                Ok(cfg) => cfg,
2031                Err(e) => {
2032                    info!(
2033                        "Failed loading kalman tracking config ({}), using defaults.",
2034                        e
2035                    );
2036                    KalmanTrackingConfig::default()
2037                }
2038            }
2039        } else {
2040            panic!("flydratrax requires saving changes to disk");
2041        }
2042    };
2043
2044    #[cfg(not(feature = "flydratrax"))]
2045    let kalman_tracking_config = KalmanTrackingConfig::default();
2046
2047    #[cfg(feature = "flydratrax")]
2048    let led_program_config = {
2049        if let ImPtDetectCfgSource::ChangedSavedToDisk(ref src) = tracker_cfg_src {
2050            // Retrieve the saved preferences
2051            let (app_info, _im_pt_detect_prefs_key) = src;
2052            match LedProgramConfig::load(app_info, LED_PROGRAM_PREFS_KEY) {
2053                Ok(cfg) => cfg,
2054                Err(e) => {
2055                    info!("Failed loading LED config ({}), using defaults.", e);
2056                    LedProgramConfig::default()
2057                }
2058            }
2059        } else {
2060            panic!("flydratrax requires saving changes to disk");
2061        }
2062    };
2063    #[cfg(not(feature = "flydratrax"))]
2064    let led_program_config = LedProgramConfig::default();
2065
2066    let cuda_devices = match nvenc::Dynlibs::new() {
2067        Ok(libs) => {
2068            match nvenc::NvEnc::new(&libs) {
2069                Ok(nv_enc) => {
2070                    let n = nv_enc.cuda_device_count()?;
2071                    let r: Result<Vec<String>> = (0..n)
2072                        .map(|i| {
2073                            let dev = nv_enc.new_cuda_device(i)?;
2074                            Ok(dev.name().map_err(nvenc::NvEncError::from)?)
2075                        })
2076                        .collect();
2077                    r?
2078                }
2079                Err(e) => {
2080                    info!(
2081                        "CUDA and nvidia-encode libraries loaded, but \
2082                        error during initialization: {}",
2083                        e,
2084                    );
2085                    // empty vector
2086                    Vec::new()
2087                }
2088            }
2089        }
2090        Err(e) => {
2091            // no cuda library, no libs
2092            info!("CUDA and nvidia-encode libraries not loaded: {}", e);
2093            // empty vector
2094            Vec::new()
2095        }
2096    };
2097    let mp4_cuda_device = if !cuda_devices.is_empty() {
2098        cuda_devices[0].as_str()
2099    } else {
2100        ""
2101    }
2102    .into();
2103
2104    #[cfg(not(feature = "fiducial"))]
2105    let apriltag_state = None;
2106
2107    #[cfg(feature = "fiducial")]
2108    let apriltag_state = Some(ApriltagState::default());
2109
2110    let im_ops_state = ImOpsState::default();
2111
2112    #[cfg(feature = "flydra_feat_detect")]
2113    let has_image_tracker_compiled = true;
2114
2115    #[cfg(not(feature = "flydra_feat_detect"))]
2116    let has_image_tracker_compiled = false;
2117
2118    let is_braid = match &args.standalone_or_braid {
2119        StandaloneOrBraid::Braid(_) => true,
2120        StandaloneOrBraid::Standalone(_) => false,
2121    };
2122
2123    // -----------------------------------------------
2124    // Check if we can use nv h264 and, if so, set that as default.
2125
2126    let ffmpeg_version = match ffmpeg_writer::ffmpeg_version() {
2127        Ok(ffmpeg_version) => Some(ffmpeg_version),
2128        Err(err) => {
2129            tracing::warn!("Could not identify ffmpeg version. {err}");
2130            None
2131        }
2132    };
2133
2134    let is_nvenc_functioning = test_nvenc_save(frame.image.borrow())?;
2135
2136    let mp4_codec = match is_nvenc_functioning {
2137        true => CodecSelection::H264Nvenc,
2138        // Default to ffmpeg libx264 (shown in the browser as "ffmpeg -c:v
2139        // libx264"). Must exactly match an entry in the `CodecSelection`
2140        // `EnumIter` list so the frontend selects it as an available codec.
2141        false => CodecSelection::Ffmpeg(FfmpegCodecArgs {
2142            codec: Some("libx264".to_string()),
2143            ..Default::default()
2144        }),
2145    };
2146
2147    #[cfg(target_os = "macos")]
2148    let is_videotoolbox_functioning = true;
2149
2150    #[cfg(not(target_os = "macos"))]
2151    let is_videotoolbox_functioning = false;
2152
2153    // -----------------------------------------------
2154
2155    let mp4_filename_template = args
2156        .mp4_filename_template
2157        .replace("{CAMNAME}", raw_cam_name.as_str());
2158    let fmf_filename_template = args
2159        .fmf_filename_template
2160        .replace("{CAMNAME}", raw_cam_name.as_str());
2161    let ufmf_filename_template = args
2162        .ufmf_filename_template
2163        .replace("{CAMNAME}", raw_cam_name.as_str());
2164
2165    #[cfg(feature = "fiducial")]
2166    let format_str_apriltag_csv = args
2167        .apriltag_csv_filename_template
2168        .replace("{CAMNAME}", use_camera_name);
2169
2170    #[cfg(not(feature = "fiducial"))]
2171    let format_str_apriltag_csv = "".into();
2172
2173    #[cfg(feature = "flydratrax")]
2174    let has_flydratrax_compiled = true;
2175
2176    #[cfg(not(feature = "flydratrax"))]
2177    let has_flydratrax_compiled = false;
2178
2179    let shared_store = ChangeTracker::new(StoreType {
2180        is_braid,
2181        ffmpeg_version,
2182        is_nvenc_functioning,
2183        is_videotoolbox_functioning,
2184        is_recording_mp4: None,
2185        is_recording_fmf: None,
2186        is_recording_ufmf: None,
2187        format_str_apriltag_csv,
2188        format_str_mp4: mp4_filename_template,
2189        format_str: fmf_filename_template,
2190        format_str_ufmf: ufmf_filename_template,
2191        camera_name: cam.name().into(),
2192        camera_gamma,
2193        recording_filename: None,
2194        mp4_bitrate: Default::default(),
2195        mp4_codec,
2196        mp4_max_framerate: Default::default(),
2197        mp4_cuda_device,
2198        gain: gain_ranged,
2199        gain_auto,
2200        exposure_time: exposure_ranged,
2201        exposure_auto,
2202        frame_rate_limit_enabled,
2203        frame_rate_limit,
2204        trigger_mode,
2205        trigger_selector,
2206        image_width,
2207        image_height,
2208        is_doing_object_detection: false,
2209        measured_fps: 0.0,
2210        is_saving_im_pt_detect_csv: None,
2211        has_image_tracker_compiled,
2212        im_pt_detect_cfg: im_pt_detect_cfg.clone(),
2213        has_flydratrax_compiled,
2214        kalman_tracking_config,
2215        led_program_config,
2216        led_box_device_lost: false,
2217        led_box_device_state: None,
2218        led_box_device_path: args.led_box_device_path.clone(),
2219        #[cfg(feature = "checkercal")]
2220        has_checkercal_compiled: true,
2221        #[cfg(not(feature = "checkercal"))]
2222        has_checkercal_compiled: false,
2223        checkerboard_data: strand_cam_storetype::CheckerboardCalState::default(),
2224        checkerboard_save_debug: None,
2225        post_trigger_buffer_size: 0,
2226        cuda_devices,
2227        apriltag_state,
2228        im_ops_state,
2229        had_frame_processing_error: false,
2230        camera_calibration: None,
2231        version_update: None,
2232    });
2233
2234    let frame_processing_error_state = Arc::new(RwLock::new(FrameProcessingErrorState::default()));
2235
2236    // let mut config = get_default_config();
2237    // config.cookie_name = "strand-camclient".to_string();
2238
2239    let mut shared_store_changes_rx = shared_store.get_changes(1);
2240
2241    // A channel for the data sent from the client browser.
2242    let (firehose_callback_tx, firehose_callback_rx) = tokio::sync::mpsc::channel(10);
2243
2244    let callback_senders = StrandCamCallbackSenders {
2245        cam_args_tx: cam_args_tx.clone(),
2246        firehose_callback_tx,
2247        led_box_tx_std: led_box_tx_std.clone(),
2248        tx_frame: tx_frame.clone(),
2249    };
2250
2251    let (tx_new_connection, rx_new_connection) = tokio::sync::mpsc::channel(10);
2252
2253    let shared_state = Arc::new(RwLock::new(shared_store));
2254    let shared_store_arc = shared_state.clone();
2255
2256    // Create our app state.
2257    let app_state = StrandCamAppState {
2258        cam_name: cam.name().to_string(),
2259        event_broadcaster: Default::default(),
2260        callback_senders,
2261        tx_new_connection,
2262        shared_store_arc,
2263        bui_server_info: http_camserver_info.clone(),
2264        persistent_secret: persistent_secret.clone(),
2265    };
2266
2267    let shared_store_arc = shared_state.clone();
2268
2269    // This future will send state updates to all connected event listeners.
2270    let event_broadcaster = app_state.event_broadcaster.clone();
2271    // A separate handle for the command task, which broadcasts a final "quit"
2272    // event to all clients during shutdown.
2273    let quit_event_broadcaster = app_state.event_broadcaster.clone();
2274    let send_updates_future = async move {
2275        while let Some((_prev_state, next_state)) = shared_store_changes_rx.next().await {
2276            let chunk = to_event_chunk(&next_state);
2277            event_broadcaster.broadcast_frame(chunk).await;
2278        }
2279    };
2280
2281    let trusted_networks = braid_types::parse_trusted_networks(&args.trusted_networks)?;
2282    let router = http_router::build_http_router(
2283        persistent_secret,
2284        trusted_networks,
2285        http_camserver_info.token(),
2286        app_state,
2287    )?;
2288
2289    // create future for our app
2290    let http_serve_future = {
2291        use std::future::IntoFuture;
2292        axum::serve(
2293            listener,
2294            router.into_make_service_with_connect_info::<SocketAddr>(),
2295        )
2296        .into_future()
2297    };
2298
2299    let urls = strand_bui_backend_session::build_urls(&http_camserver_info)?;
2300
2301    #[cfg(feature = "eframe-gui")]
2302    {
2303        // Loop until GUI from other thread is available.
2304        loop {
2305            {
2306                // scope for holding lock
2307                let mut my_guard = gui_singleton.lock().unwrap();
2308
2309                // http_camserver_info
2310                // Set URL
2311                my_guard.url = Some(format!("{}", urls[0]));
2312
2313                // Ensure URL is drawn
2314                if let Some(ctx_ref) = my_guard.ctx.as_ref() {
2315                    ctx_ref.request_repaint();
2316                    // We have GUI, exit wait loop.
2317                    break;
2318                }
2319            }
2320            // Wait a bit and check if GUI has launched again.
2321            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2322        }
2323    }
2324
2325    #[cfg_attr(not(feature = "eframe-gui"), expect(clippy::let_unit_value))]
2326    let _ = gui_singleton;
2327
2328    // Display where we are listening.
2329    if is_braid {
2330        debug!("Strand Cam listening at {listen_addr}");
2331    } else {
2332        info!("Strand Cam listening at {listen_addr}");
2333
2334        for url in urls.iter() {
2335            info!(" * predicted URL {url}");
2336            if !braid_types::is_loopback(url) {
2337                println!("QR code for {url}");
2338                display_qr_url(&format!("{url}"))?;
2339            }
2340        }
2341    }
2342
2343    #[cfg(feature = "checkercal")]
2344    let collected_corners_arc: CollectedCornersArc = Arc::new(RwLock::new(Vec::new()));
2345
2346    let frame_process_task_fut = {
2347        #[cfg(feature = "flydra_feat_detect")]
2348        let csv_save_dir = args.csv_save_dir.clone();
2349
2350        #[cfg(feature = "flydratrax")]
2351        let model_server_addr = args.model_server_addr;
2352
2353        #[cfg(feature = "flydratrax")]
2354        let led_box_tx_std = led_box_tx_std.clone();
2355        #[cfg(feature = "flydratrax")]
2356        let http_camserver_info2 = http_camserver_info.clone();
2357        let led_box_heartbeat_update_arc2 = led_box_heartbeat_update_arc.clone();
2358        #[cfg(feature = "flydratrax")]
2359        let model_server_data_tx = {
2360            info!("send_pose server at {model_server_addr}");
2361            let (model_server_data_tx, data_rx) = tokio::sync::mpsc::channel(50);
2362            let model_server_future = flydra2::new_model_server(data_rx, model_server_addr);
2363            tokio::spawn(model_server_future);
2364            model_server_data_tx
2365        };
2366
2367        let cam_name2 = raw_cam_name.clone();
2368        frame_process_task(
2369            #[cfg(feature = "flydratrax")]
2370            model_server_data_tx,
2371            cam_name2,
2372            #[cfg(feature = "flydra_feat_detect")]
2373            camera_cfg,
2374            #[cfg(feature = "flydra_feat_detect")]
2375            image_width,
2376            #[cfg(feature = "flydra_feat_detect")]
2377            image_height,
2378            rx_frame,
2379            #[cfg(feature = "flydra_feat_detect")]
2380            im_pt_detect_cfg,
2381            #[cfg(feature = "flydra_feat_detect")]
2382            std::path::Path::new(&csv_save_dir).to_path_buf(),
2383            firehose_tx,
2384            #[cfg(feature = "flydratrax")]
2385            led_box_tx_std,
2386            #[cfg(feature = "flydratrax")]
2387            http_camserver_info2,
2388            transmit_msg_tx.clone(),
2389            camdata_udp_addr,
2390            led_box_heartbeat_update_arc2,
2391            #[cfg(feature = "checkercal")]
2392            collected_corners_arc.clone(),
2393            #[cfg(feature = "flydratrax")]
2394            &args,
2395            #[cfg(feature = "flydra_feat_detect")]
2396            acquisition_duration_allowed_imprecision_msec,
2397            #[cfg(feature = "flydra_feat_detect")]
2398            app_name,
2399            device_clock_model,
2400            local_and_cam_time0,
2401            trigger_type,
2402            #[cfg(target_os = "linux")]
2403            v4l_out_stream,
2404            data_dir,
2405        )
2406    };
2407    debug!("frame_process_task spawned");
2408
2409    tx_frame
2410        .send(Msg::Store(shared_store_arc.clone()))
2411        .await
2412        .unwrap();
2413
2414    debug!("installing frame stream handler");
2415
2416    // install frame handling
2417    let n_buffered_frames = 100;
2418    let frame_stream = cam.frames(n_buffered_frames)?;
2419    let cam_stream_future = cam_stream_task::run_cam_stream_task(
2420        frame_stream,
2421        tx_frame,
2422        shared_store_arc.clone(),
2423        frame_processing_error_state.clone(),
2424        transmit_msg_tx.clone(),
2425        raw_cam_name.clone(),
2426        send_image_to_braid_interval,
2427        gui_stuff2,
2428    );
2429
2430    let do_version_check = match std::env::var_os("DISABLE_VERSION_CHECK") {
2431        Some(v) => &v == "0",
2432        None => true,
2433    };
2434
2435    if do_version_check {
2436        let app_version: semver::Version = {
2437            let mut my_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
2438            my_version.build = semver::BuildMetadata::new(env!("GIT_HASH").to_string().as_str())?;
2439            my_version
2440        };
2441
2442        info!(
2443            "Welcome to {} {}. For more details \
2444            contact Andrew Straw <straw@bio.uni-freiburg.de>. This program will check for new \
2445            versions automatically. To disable printing this message and checking for new \
2446            versions, set the environment variable DISABLE_VERSION_CHECK=1.",
2447            app_name, app_version,
2448        );
2449
2450        let store_for_version_check = shared_store_arc.clone();
2451        // Build the version-check client once and reuse it for every check.
2452        let checker = strand_version_check::VersionChecker::new();
2453
2454        // Check now and every 30 minutes.
2455        let interval_stream = tokio::time::interval(std::time::Duration::from_secs(1800));
2456        let mut interval_stream = tokio_stream::wrappers::IntervalStream::new(interval_stream);
2457
2458        let stream_future = async move {
2459            // The newest version this client knows about. Starts as our own
2460            // version and is advanced as the server reports newer ones, so each
2461            // new version is announced only once.
2462            let mut known_version = app_version;
2463            while interval_stream.next().await.is_some() {
2464                let user_agent = format!("{}/{}", app_name, known_version);
2465                if let Some(av) = checker.fetch("strand-cam", &user_agent).await
2466                    && av.version > known_version
2467                {
2468                    info!(
2469                        "New version of {} is available: {}. {}",
2470                        app_name, av.version, av.message
2471                    );
2472                    // Surface to every connected browser as a dismissible banner.
2473                    let update = strand_cam_storetype::VersionUpdate {
2474                        available: av.version.to_string(),
2475                        message: av.message,
2476                        url: av.url,
2477                    };
2478                    let mut tracker = store_for_version_check.write().unwrap();
2479                    tracker.modify(|store| store.version_update = Some(update));
2480                    known_version = av.version;
2481                }
2482            }
2483            debug!("version check future done {}:{}", file!(), line!());
2484        };
2485        tokio::spawn(Box::pin(stream_future));
2486        debug!("version check future spawned {}:{}", file!(), line!());
2487    }
2488
2489    tokio::spawn(Box::pin(cam_stream_future));
2490    debug!("cam_stream_future future spawned {}:{}", file!(), line!());
2491
2492    let cam_arg_future = {
2493        #[cfg(feature = "checkercal")]
2494        let cam_name2 = raw_cam_name.clone();
2495
2496        cam_arg_task::run_cam_arg_task(
2497            cam,
2498            cam_args_rx,
2499            shared_store_arc.clone(),
2500            quit_event_broadcaster,
2501            frame_processing_error_state,
2502            transmit_msg_tx,
2503            current_cam_settings_extension,
2504            raw_cam_name,
2505            tx_frame2,
2506            #[cfg(feature = "flydra_feat_detect")]
2507            tracker_cfg_src,
2508            #[cfg(feature = "checkercal")]
2509            cam_name2,
2510            #[cfg(feature = "checkercal")]
2511            collected_corners_arc,
2512            #[cfg(feature = "checkercal")]
2513            image_width,
2514            #[cfg(feature = "checkercal")]
2515            image_height,
2516        )
2517    };
2518
2519    let (launched_tx, mut launched_rx) = tokio::sync::watch::channel(());
2520
2521    #[cfg(not(feature = "eframe-gui"))]
2522    let no_browser = args.no_browser;
2523
2524    // Never launch browser automatically in GUI mode.
2525    #[cfg(feature = "eframe-gui")]
2526    let no_browser = true;
2527
2528    if !no_browser {
2529        // Spawn a task which first waits for the Strand Cam webserver to be
2530        // ready and then itself opens a browser.
2531        let _launcher_task = tokio::spawn(async move {
2532            // Let the webserver start before opening browser.
2533            launched_rx.changed().await.unwrap();
2534            let url = format!("{}", urls[0]);
2535            let blocking_task = tokio::task::spawn_blocking(move || {
2536                info!("Opening browser at {}", url);
2537                match webbrowser::open(&url) {
2538                    Ok(_) => trace!("Browser opened"),
2539                    Err(e) => error!("Error opening brower: {:?}", e),
2540                };
2541                debug!("browser thread done {}:{}", file!(), line!());
2542            });
2543            blocking_task.await?;
2544            Ok::<_, eyre::Report>(())
2545        });
2546    }
2547
2548    let firehose_task_join_handle = tokio::spawn(async {
2549        // The first thing this task does is pop a frame from firehose_rx, so we
2550        // should ensure there is one present.
2551        video_streaming::firehose_task(rx_new_connection, firehose_rx, firehose_callback_rx)
2552            .await
2553            .unwrap();
2554    });
2555
2556    debug!("  running forever");
2557
2558    led_box_task::run_led_box_task(
2559        led_box_tx_std,
2560        led_box_rx,
2561        led_box_heartbeat_update_arc,
2562        shared_store_arc,
2563    )
2564    .await?;
2565    // _dummy_tx is not dropped until after `select!` below. It will never send.
2566    let (_dummy_tx, dummy_rx) = tokio::sync::mpsc::channel(1);
2567    let mut quit_rx = match quit_rx {
2568        None => dummy_rx,
2569        Some(fut) => fut,
2570    };
2571
2572    // Now run until first future returns, then exit.
2573    info!("Strand Cam launched.");
2574    launched_tx.send(())?;
2575    tokio::select! {
2576        res = http_serve_future => {res?},
2577        res = cam_arg_future => {res?},
2578        res = forward_to_mainbrain_fut => {res?},
2579        _ = send_updates_future => {},
2580        res = frame_process_task_fut => {res?},
2581        res = firehose_task_join_handle => {res?},
2582        _ = quit_rx.recv() => {},
2583    }
2584    info!("Strand Cam ending nicely. :)");
2585    // All other futures above are now dropped, thus cancelled.
2586
2587    Ok(mymod)
2588}
2589
2590fn measure_times<C>(cam: &C) -> Result<(chrono::DateTime<chrono::Utc>, i64)>
2591where
2592    C: ci2::Camera,
2593{
2594    let start = chrono::Utc::now();
2595    cam.command_execute("TimestampLatch", true)?;
2596    let remote = cam.feature_int("TimestampLatchValue")?;
2597    let stop = chrono::Utc::now();
2598    let max_err = stop - start;
2599    // assume symmetric delay
2600    let remote_offset_symmetric = max_err / 2;
2601    let remote_in_local = start + remote_offset_symmetric;
2602    tracing::debug!("Camera timestamp: {remote_in_local} {remote} {max_err}.");
2603    Ok((remote_in_local, remote))
2604}
2605
2606async fn send_cam_settings_to_braid(
2607    cam_settings: &str,
2608    transmit_msg_tx: &tokio::sync::mpsc::Sender<braid_types::BraidHttpApiCallback>,
2609    current_cam_settings_extension: &str,
2610    raw_cam_name: &RawCamName,
2611) -> StdResult<(), tokio::sync::mpsc::error::SendError<braid_types::BraidHttpApiCallback>> {
2612    let current_cam_settings_buf = cam_settings.to_string();
2613    let current_cam_settings_extension = current_cam_settings_extension.to_string();
2614    let raw_cam_name = raw_cam_name.clone();
2615    let transmit_msg_tx = transmit_msg_tx.clone();
2616
2617    let msg = braid_types::BraidHttpApiCallback::UpdateCamSettings(braid_types::PerCam {
2618        raw_cam_name,
2619        inner: braid_types::UpdateCamSettings {
2620            current_cam_settings_buf,
2621            current_cam_settings_extension,
2622        },
2623    });
2624    transmit_msg_tx.send(msg).await
2625}
2626
2627fn bitrate_to_u32(br: &strand_cam_remote_control::BitrateSelection) -> Option<u32> {
2628    use strand_cam_remote_control::BitrateSelection::*;
2629    Some(match br {
2630        Bitrate500 => 500,
2631        Bitrate1000 => 1000,
2632        Bitrate2000 => 2000,
2633        Bitrate3000 => 3000,
2634        Bitrate4000 => 4000,
2635        Bitrate5000 => 5000,
2636        Bitrate10000 => 10000,
2637        BitrateUnlimited => return None,
2638    })
2639}
2640
2641struct FinalMp4RecordingConfig {
2642    final_cfg: strand_cam_remote_control::RecordingConfig,
2643}
2644
2645impl FinalMp4RecordingConfig {
2646    fn new(shared: &StoreType, creation_time: chrono::DateTime<chrono::Local>) -> Self {
2647        let mp4_codec = match shared.mp4_codec {
2648            CodecSelection::H264Nvenc => {
2649                let cuda_device = shared
2650                    .cuda_devices
2651                    .iter()
2652                    .position(|x| x == &shared.mp4_cuda_device)
2653                    .unwrap_or(0);
2654                let cuda_device = cuda_device.try_into().unwrap();
2655                Some(Mp4Codec::H264NvEnc(NvidiaH264Options {
2656                    bitrate: bitrate_to_u32(&shared.mp4_bitrate),
2657                    cuda_device,
2658                }))
2659            }
2660            CodecSelection::H264OpenH264 => {
2661                let preset = strand_cam_remote_control::OpenH264Preset::AllFrames;
2662                if shared.mp4_bitrate
2663                    != strand_cam_remote_control::BitrateSelection::BitrateUnlimited
2664                {
2665                    warn!("ignoring mp4 bitrate with OpenH264 codec");
2666                }
2667                Some(Mp4Codec::H264OpenH264(
2668                    strand_cam_remote_control::OpenH264Options {
2669                        debug: false,
2670                        preset,
2671                    },
2672                ))
2673            }
2674            _ => None,
2675        };
2676        let h264_metadata = {
2677            let mut h264_metadata =
2678                strand_cam_remote_control::H264Metadata::new("strand-cam", creation_time.into());
2679            h264_metadata.camera_name = Some(shared.camera_name.clone());
2680            h264_metadata.gamma = shared.camera_gamma;
2681            Some(h264_metadata)
2682        };
2683        let final_cfg = if let Some(codec) = mp4_codec {
2684            let final_cfg = Mp4RecordingConfig {
2685                codec,
2686                max_framerate: shared.mp4_max_framerate.clone(),
2687                h264_metadata,
2688            };
2689            strand_cam_remote_control::RecordingConfig::Mp4(final_cfg)
2690        } else {
2691            use strand_cam_remote_control::CodecSelection::*;
2692            let codec = match &shared.mp4_codec {
2693                H264Nvenc | H264OpenH264 => {
2694                    unreachable!();
2695                }
2696                Ffmpeg(args) => args.clone(),
2697            };
2698            strand_cam_remote_control::RecordingConfig::Ffmpeg(FfmpegRecordingConfig {
2699                codec_args: codec,
2700                max_framerate: shared.mp4_max_framerate.clone(),
2701                h264_metadata,
2702            })
2703        };
2704        FinalMp4RecordingConfig { final_cfg }
2705    }
2706}
2707
2708fn to_eyre<T>(e: SendError<T>) -> eyre::Report {
2709    eyre!("SendError: {e} {e:?}")
2710}
2711
2712#[cfg(test)]
2713mod tests {
2714    use super::*;
2715
2716    use std::net::Ipv4Addr;
2717
2718    // ---- Layer 1: `find_local_ip_for_remote` ----
2719    //
2720    // These exercise the real UDP-connect routing primitive. UDP `connect()`
2721    // sends no packets, so these do not require actual network connectivity,
2722    // only that the relevant route/interface exists. Cases that depend on
2723    // optional setup (IPv6, a non-loopback interface) skip rather than fail.
2724
2725    #[test]
2726    fn local_ip_for_loopback_is_loopback() {
2727        let local = find_local_ip_for_remote(IpAddr::V4(Ipv4Addr::LOCALHOST))
2728            .expect("resolving local IP for IPv4 loopback should always succeed");
2729        assert!(
2730            local.is_loopback(),
2731            "expected loopback source for loopback remote, got {local}"
2732        );
2733    }
2734
2735    #[test]
2736    fn local_ip_for_ipv6_loopback_is_loopback() {
2737        // Skip on hosts without IPv6 (some CI environments).
2738        if UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)).is_err() {
2739            eprintln!("skipping: no IPv6 support on this host");
2740            return;
2741        }
2742        let local = find_local_ip_for_remote(IpAddr::V6(Ipv6Addr::LOCALHOST))
2743            .expect("resolving local IP for IPv6 loopback should succeed when IPv6 is present");
2744        assert!(
2745            local.is_loopback(),
2746            "expected loopback source for loopback remote, got {local}"
2747        );
2748    }
2749
2750    /// Connecting to one of the machine's own non-loopback interface IPs makes
2751    /// the OS select that same IP as the source. This is the property that lets
2752    /// a single node simulate a "remote" Braid in a future integration test.
2753    #[test]
2754    fn local_ip_for_own_interface_is_that_interface() {
2755        // Find a non-loopback IPv4 address that is actually bindable on this
2756        // host (i.e. assigned to a local interface). `0` lets the OS pick a
2757        // free port; a successful bind proves the IP is local.
2758        let Some(local_iface_ip) = local_non_loopback_ipv4() else {
2759            eprintln!("skipping: no non-loopback IPv4 interface on this host");
2760            return;
2761        };
2762        let resolved = find_local_ip_for_remote(local_iface_ip)
2763            .expect("resolving local IP for an own interface IP should succeed");
2764        assert_eq!(
2765            resolved, local_iface_ip,
2766            "expected own interface IP {local_iface_ip} to resolve to itself, got {resolved}"
2767        );
2768    }
2769
2770    /// Return a non-loopback IPv4 address assigned to this host, if any.
2771    ///
2772    /// Rather than enumerating interfaces (which needs a platform-specific
2773    /// crate), we probe a few common destinations and treat the resulting
2774    /// non-loopback source IP as a known-local interface address.
2775    fn local_non_loopback_ipv4() -> Option<IpAddr> {
2776        // Public IPs route via the default gateway interface (if one exists);
2777        // the documentation/TEST-NET ranges are used purely as routing targets,
2778        // no packets are sent.
2779        for probe in ["8.8.8.8", "192.0.2.1", "1.1.1.1"] {
2780            let remote: IpAddr = probe.parse().unwrap();
2781            if let Ok(local) = find_local_ip_for_remote(remote)
2782                && !local.is_loopback()
2783                && local.is_ipv4()
2784            {
2785                return Some(local);
2786            }
2787        }
2788        None
2789    }
2790
2791    // ---- Layer 2: `braid_strand_cam_http_address` decision logic ----
2792    //
2793    // Pure logic, tested with an injected resolver so no sockets are involved.
2794
2795    const LOOPBACK: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
2796    const REMOTE: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10));
2797
2798    fn resolver_ok(ip: IpAddr) -> impl FnOnce(IpAddr) -> std::io::Result<IpAddr> {
2799        move |_remote| Ok(ip)
2800    }
2801
2802    fn resolver_err() -> impl FnOnce(IpAddr) -> std::io::Result<IpAddr> {
2803        |_remote| Err(std::io::Error::other("simulated resolver failure"))
2804    }
2805
2806    #[test]
2807    fn loopback_braid_no_override_binds_loopback() {
2808        let addr = braid_strand_cam_http_address(LOOPBACK, None, resolver_ok(REMOTE));
2809        assert_eq!(addr, "127.0.0.1:0");
2810    }
2811
2812    #[test]
2813    fn remote_braid_no_override_uses_resolved_local_ip() {
2814        let local = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20));
2815        let addr = braid_strand_cam_http_address(REMOTE, None, resolver_ok(local));
2816        assert_eq!(addr, "192.168.1.20:0");
2817    }
2818
2819    #[test]
2820    fn remote_braid_resolver_failure_falls_back_to_loopback() {
2821        let addr = braid_strand_cam_http_address(REMOTE, None, resolver_err());
2822        assert_eq!(addr, "127.0.0.1:0");
2823    }
2824
2825    #[test]
2826    fn override_is_used_verbatim_for_remote_braid() {
2827        // The override must win even though the resolver would return something
2828        // else; the resolver must not even be consulted.
2829        let addr = braid_strand_cam_http_address(REMOTE, Some("10.0.0.5:1234".to_string()), |_| {
2830            panic!("resolver must not be called when an override is present")
2831        });
2832        assert_eq!(addr, "10.0.0.5:1234");
2833    }
2834
2835    #[test]
2836    fn override_is_used_verbatim_for_loopback_braid() {
2837        let addr =
2838            braid_strand_cam_http_address(LOOPBACK, Some("0.0.0.0:44444".to_string()), |_| {
2839                panic!("resolver must not be called when an override is present")
2840            });
2841        assert_eq!(addr, "0.0.0.0:44444");
2842    }
2843}