1use 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
37fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum FpsTimestampSource {
186 Trigger,
192 Hardware,
197 HostClock,
202}
203
204pub struct FpsCalc {
205 prev: Option<(usize, i128)>,
207 frames_to_average: usize,
208 source: Option<FpsTimestampSource>,
212}
213
214impl FpsCalc {
215 pub fn new(frames_to_average: usize) -> Self {
217 Self {
218 prev: None,
219 frames_to_average,
220 source: None,
221 }
222 }
223 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 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 #[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; let mut measured = None;
276 for fno in 0..=100usize {
277 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 #[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; 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 #[test]
317 fn host_clock_overreads_under_bursty_delivery() {
318 let mut fc = FpsCalc::new(100);
319 let true_fps = 30.0;
320 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 #[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 assert_eq!(fc.update(2, 999_999, FpsTimestampSource::HostClock), None);
342 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
395fn 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 socket.connect(SocketAddr::new(remote_ip, 1))?;
411 Ok(socket.local_addr()?.ip())
412}
413
414fn 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 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 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 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
499trait 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 bui_server_info: strand_bui_backend_session_types::BuiServerAddrInfo,
539 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)]
562pub 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, 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#[derive(Debug, Default, Clone)]
613pub struct BraidArgs {
614 pub braid_url: String,
615 pub camera_name: String,
616}
617
618#[derive(Debug, Clone, Default)]
620pub struct StandaloneArgs {
621 pub camera_name: Option<String>,
622 pub http_server_addr: Option<String>,
624 pub pixel_format: Option<String>,
625 pub force_camera_sync_mode: bool,
627 pub software_limit_framerate: StartSoftwareFrameRateLimit,
632 pub acquisition_duration_allowed_imprecision_msec: Option<f64>,
640 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
658pub(crate) const MP4_FILENAME_TEMPLATE_DEFAULT: &str = "movie%Y%m%d_%H%M%S.%f_{CAMNAME}.mp4";
660pub(crate) const FMF_FILENAME_TEMPLATE_DEFAULT: &str = "movie%Y%m%d_%H%M%S.%f_{CAMNAME}.fmf";
662pub(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 pub standalone_or_braid: StandaloneOrBraid,
669 pub secret: Option<String>,
671 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 PseudoCal,
703 XmlFile(std::path::PathBuf),
705 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 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 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
800async 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 let key = ConnectionSessionKey::new(session_key.0, addr);
817
818 let (conn_tx, body) = app_state.event_broadcaster.new_connection(key);
823
824 {
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 tracing::debug!("initial send error");
836 }
837 }
838 }
839
840 {
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 tracing::debug!("initial send error");
849 }
850 }
851 }
852
853 {
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
884async 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
895fn 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 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 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#[derive(Debug)]
1040struct BraidInfo {
1041 mainbrain_bui_loc: BuiServerAddrInfo,
1042
1043 mainbrain_session: braid_http_session::MainbrainSession,
1044 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
1053struct 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 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
1085pub 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 home::home_dir().ok_or_else(|| {
1104 eyre::eyre!("Could not determine home directory and data directory not set.")
1105 })?,
1106 PathBuf::from("."),
1108 )
1109 };
1110
1111 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 #[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 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 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 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
1236async 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 let jar = jar.read().unwrap();
1262 Preferences::save(&*jar, &APP_INFO, BRAID_COOKIE_KEY)?;
1263 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 log_dir: PathBuf,
1294 data_dir: PathBuf,
1296 log_file_time: chrono::DateTime<chrono::Local>,
1297}
1298
1299async 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 let from_mainbrain = connect_to_braid(braid_args).await?;
1318
1319 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 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 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 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
1417async 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 mainbrain_session
1430 .post_callback_message(msg)
1431 .await
1432 .context("failed sending message to mainbrain")?;
1433 }
1434 Ok(())
1435}
1436
1437#[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; 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 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 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 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 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 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 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 #[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 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 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 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 tracing::debug!("PTP clock offset {ptp_offset_from_master} nanoseconds.");
1723 if ptp_offset_from_master.abs() < clock_sync_threshold_nanos {
1724 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 cam.acquisition_start()?;
1747 let (tx_frame, rx_frame) = tokio::sync::mpsc::channel::<Msg>(20);
1749 let tx_frame2 = tx_frame.clone();
1750
1751 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 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 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 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 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 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 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 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 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 ¤t_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 cam.set_software_frame_rate_limit(*fps_limit).unwrap();
2002 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 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 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 Vec::new()
2087 }
2088 }
2089 }
2090 Err(e) => {
2091 info!("CUDA and nvidia-encode libraries not loaded: {}", e);
2093 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 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 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 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 shared_store_changes_rx = shared_store.get_changes(1);
2240
2241 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 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 let event_broadcaster = app_state.event_broadcaster.clone();
2271 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 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 {
2305 {
2306 let mut my_guard = gui_singleton.lock().unwrap();
2308
2309 my_guard.url = Some(format!("{}", urls[0]));
2312
2313 if let Some(ctx_ref) = my_guard.ctx.as_ref() {
2315 ctx_ref.request_repaint();
2316 break;
2318 }
2319 }
2320 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 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 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 let checker = strand_version_check::VersionChecker::new();
2453
2454 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 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 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 #[cfg(feature = "eframe-gui")]
2526 let no_browser = true;
2527
2528 if !no_browser {
2529 let _launcher_task = tokio::spawn(async move {
2532 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 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 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 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 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 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 #[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 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 #[test]
2754 fn local_ip_for_own_interface_is_that_interface() {
2755 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 fn local_non_loopback_ipv4() -> Option<IpAddr> {
2776 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 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 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}