1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#![cfg_attr(feature = "backtrace", feature(error_generic_member_access))]

use serde::{Deserialize, Serialize};

use flydra_types::{BraidCameraConfig, FakeSyncConfig, TriggerType, TriggerboxConfig};

/// The Braid configuration error type.
#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("lookup error on variable: {source}")]
    ShellExpandLookupVarError {
        #[from]
        source: shellexpand::LookupError<std::env::VarError>,
        #[cfg(feature = "backtrace")]
        backtrace: std::backtrace::Backtrace,
    },
    #[error("IO error: {source}")]
    IoError {
        #[from]
        source: std::io::Error,
        #[cfg(feature = "backtrace")]
        backtrace: std::backtrace::Backtrace,
    },
    #[error("TOML deserialization error: {source}")]
    TomlDeError {
        #[from]
        source: toml::de::Error,
        #[cfg(feature = "backtrace")]
        backtrace: std::backtrace::Backtrace,
    },
}

type Result<T> = std::result::Result<T, Error>;

/// The default value for [MainbrainConfig::http_api_server_addr].
pub const DEFAULT_HTTP_API_SERVER_ADDR: &str = "127.0.0.1:0";

fn default_http_api_server_addr() -> String {
    DEFAULT_HTTP_API_SERVER_ADDR.to_string()
}

/// The default value for [MainbrainConfig::output_base_dirname].
pub const DEFAULT_OUTPUT_BASE_DIRNAME: &str = "~/BRAID-DATA";

fn default_output_base_dirname() -> std::path::PathBuf {
    DEFAULT_OUTPUT_BASE_DIRNAME.into()
}

fn default_model_server_addr() -> std::net::SocketAddr {
    flydra_types::DEFAULT_MODEL_SERVER_ADDR.parse().unwrap()
}

fn default_true() -> bool {
    true
}

/// Split `path` (which must be a file) into directory and filename component.
fn split_path<P: AsRef<std::path::Path>>(path: P) -> (std::path::PathBuf, std::path::PathBuf) {
    let path = path.as_ref();
    assert!(path.is_file());
    let mut components = path.components();
    let filename = components.next_back().unwrap().as_os_str().into();
    let dirname = components.as_path().into();
    (dirname, filename)
}

/// If `path` is relative, make it relative to `dirname`.
///
/// `path` must be utf-8 encoded and can start with a tilde, which is expanded
/// to the home directory.
fn fixup_relative_path(path: &mut std::path::PathBuf, dirname: &std::path::Path) -> Result<()> {
    let pathstr = path.as_os_str().to_str().unwrap();
    let expanded = shellexpand::full(&pathstr)?;
    *path = std::path::PathBuf::from(expanded.to_string());

    if path.is_relative() {
        *path = dirname.join(&path);
    }
    Ok(())
}

/// The sub-configuration of [BraidConfig] for `mainbrain` - the central
/// component of Braid that integrates information from multiple cameras and
/// performs tracking.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MainbrainConfig {
    /// Filename of the camera calibration, optional.
    ///
    /// Can contain shell variables such as `~`, `$A`, or `${B}`.
    ///
    /// If the filename ends with .pymvg or .json, it will be treated as a pymvg
    /// calibration file. Else it will be treated considered in the flydra XML
    /// calibration format.
    pub cal_fname: Option<std::path::PathBuf>,
    /// Directory where data should be saved. Can contain shell variables.
    /// Defaults to [DEFAULT_OUTPUT_BASE_DIRNAME].
    #[serde(default = "default_output_base_dirname")]
    pub output_base_dirname: std::path::PathBuf,
    /// Parameters for Kalman filter and data association
    #[serde(default = "flydra_types::default_tracking_params_full_3d")]
    pub tracking_params: flydra_types::TrackingParams,
    // Raising the mainbrain thread priority is currently disabled.
    // /// Parameters to potentially raise the mainbrain thread priority.
    // sched_policy_priority: Option<(i32, i32)>,
    /// Address of UDP port to send low-latency detection data
    pub lowlatency_camdata_udp_addr: Option<String>,
    #[serde(default)]
    pub lowlatency_camdata_udp_port: u16,
    /// Address of HTTP port for control API. This is specified in the format
    /// `IP:PORT` where:
    ///
    /// `IP` can be:
    ///  - a numerical IPv4 address:
    ///    - e.g. `1.1.1.1` uses the specific IP
    ///    - `127.0.0.1` for the loopback interface
    ///    - `0.0.0.0` to open the server on all available IPv4 interfaces
    ///  - a numerical IPv6 address:
    ///    - e.g. `[2001:db8:3333:4444:5555:6666:7777:8888]` uses the specific
    ///      IP
    ///    - `[::1]` for the loopback interface
    ///    - `[::]` to open the server on all available IPv6 interfaces
    ///  - a hostname which resolves to an IP address (depending on your DNS
    ///    configuration, resolves to either IPv4 or IPv6):
    ///    - `localhost` resolves to the IP address of the loopback interface
    ///    - e.g. `hostname` for a specific IP address
    ///
    /// `PORT` can be:
    ///  - `0` allows the operating system to choose an unassigned port
    ///    dynamically
    ///  - e.g. `1234` uses the specific port
    ///
    /// Set to `0.0.0.0:0` to be automatically assigned a public IP address with
    /// a dynamically assigned port.
    ///
    /// The default value is set to [DEFAULT_HTTP_API_SERVER_ADDR].
    #[serde(default = "default_http_api_server_addr")]
    pub http_api_server_addr: String,
    /// Address of HTTP port for model server emitting realtime tracking results
    #[serde(default = "default_model_server_addr")]
    pub model_server_addr: std::net::SocketAddr,
    /// Save rows to data2d_distorted where nothing detected (saves timestamps)
    #[serde(default = "default_true")]
    pub save_empty_data2d: bool,
    /// Secret to use for signing HTTP cookies (base64 encoded)
    pub secret_base64: Option<String>,
    /// For debugging: filename to store captured packet data.
    pub packet_capture_dump_fname: Option<std::path::PathBuf>,
    /// Threshold duration before logging error (msec).
    ///
    /// If the image acquisition timestamp precedes the computed trigger
    /// timestamp, clearly an error has happened. This error must lie in the
    /// computation of the trigger timestamp. This specifies the threshold error
    /// at which an error is logged. (The underlying source of such errors
    /// remains unknown.)
    pub acquisition_duration_allowed_imprecision_msec: Option<f64>,
    /// The size of the buffer, in number of messages, used by the channel for
    /// sending data to disk.
    #[serde(default = "default_write_buffer_size_num_messages")]
    pub write_buffer_size_num_messages: usize,
}

impl std::default::Default for MainbrainConfig {
    fn default() -> Self {
        Self {
            cal_fname: None,
            output_base_dirname: default_output_base_dirname(),
            tracking_params: flydra_types::default_tracking_params_full_3d(),
            // Raising the mainbrain thread priority is currently disabled.
            // sched_policy_priority: None,
            lowlatency_camdata_udp_addr: None,
            lowlatency_camdata_udp_port: Default::default(),
            http_api_server_addr: default_http_api_server_addr(),
            model_server_addr: default_model_server_addr(),
            save_empty_data2d: true,
            secret_base64: None,
            packet_capture_dump_fname: None,
            acquisition_duration_allowed_imprecision_msec:
                flydra_types::DEFAULT_ACQUISITION_DURATION_ALLOWED_IMPRECISION_MSEC,
            write_buffer_size_num_messages: default_write_buffer_size_num_messages(),
        }
    }
}

pub const fn default_write_buffer_size_num_messages() -> usize {
    10000
}

/// The Braid configuration format used in [the Braid configuration `TOML`
/// file](https://strawlab.github.io/strand-braid/braid_configuration_and_launching.html).
///
/// This is the new configuration format. Backwards compatibility is maintained
/// by first attempting to deserialize with this definition.
///
/// See the types of each field for sub-configuration values.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BraidConfig {
    /// Mainbrain configuration
    #[serde(default = "MainbrainConfig::default")]
    pub mainbrain: MainbrainConfig,
    /// Triggerbox configuration.
    #[serde(default)]
    pub trigger: TriggerType,
    pub cameras: Vec<BraidCameraConfig>,
}

impl From<BraidConfig1> for BraidConfig {
    fn from(orig: BraidConfig1) -> BraidConfig {
        let trigger = match orig.trigger {
            None => TriggerType::FakeSync(FakeSyncConfig::default()),
            Some(x) => TriggerType::TriggerboxV1(x),
        };
        BraidConfig {
            mainbrain: orig.mainbrain,
            trigger,
            cameras: orig.cameras,
        }
    }
}

/// The old configuration format.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[doc(hidden)]
pub struct BraidConfig1 {
    pub mainbrain: MainbrainConfig,
    pub trigger: Option<TriggerboxConfig>,
    pub cameras: Vec<BraidCameraConfig>,
}

impl BraidConfig {
    /// For all paths which are relative, make them relative to the
    /// config file location.
    fn fixup_relative_paths(&mut self, orig_path: &std::path::Path) -> Result<()> {
        let (dirname, _orig_path) = split_path(orig_path);

        // fixup self.mainbrain.cal_fname
        if let Some(cal_fname) = self.mainbrain.cal_fname.as_mut() {
            fixup_relative_path(cal_fname, &dirname)?;
        }

        // fixup self.mainbrain.output_base_dirname
        fixup_relative_path(&mut self.mainbrain.output_base_dirname, &dirname)?;

        // fixup self.cameras.camera_settings_filename
        for camera_config in self.cameras.iter_mut() {
            if let Some(ref mut camera_settings_filename) =
                camera_config.camera_settings_filename.as_mut()
            {
                fixup_relative_path(camera_settings_filename, &dirname)?;
            }
        }

        Ok(())
    }
}

impl std::default::Default for BraidConfig {
    fn default() -> Self {
        Self {
            mainbrain: MainbrainConfig::default(),
            // This `trigger` field has a different default than
            // TriggerType::default() in order to show the user (who will query
            // this with the `braid default-config` command) how to configure
            // the trigger box.
            trigger: TriggerType::TriggerboxV1(TriggerboxConfig::default()),
            cameras: vec![
                BraidCameraConfig::default_absdiff_config("fake-camera-1".to_string()),
                BraidCameraConfig::default_absdiff_config("fake-camera-2".to_string()),
                BraidCameraConfig::default_absdiff_config("fake-camera-3".to_string()),
            ],
        }
    }
}

/// Parse a `.toml` file and return a [BraidConfig] structure.
pub fn parse_config_file<P: AsRef<std::path::Path>>(fname: P) -> Result<BraidConfig> {
    use std::io::Read;

    let mut file = std::fs::File::open(fname.as_ref())?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let mut cfg: BraidConfig = match toml::from_str(&contents) {
        Ok(cfg) => cfg,
        Err(err_cfg2) => {
            let cfg1: BraidConfig1 = match toml::from_str(&contents) {
                Ok(cfg1) => cfg1,
                Err(err_cfg1) => {
                    log::error!(
                        "parsing config file first as BraidConfig failed \
                    and then again as BraidConfig1 failed. The parse error for \
                    BraidConfig1 is: {}\n The original error when parsing \
                    BraidConfig will now be raised.",
                        err_cfg1
                    );
                    return Err(err_cfg2.into());
                }
            };
            BraidConfig::from(cfg1)
        }
    };
    // let mut cfg: BraidConfig = toml::from_str(&contents)?;
    cfg.fixup_relative_paths(fname.as_ref())?;
    Ok(cfg)
}