Skip to main content

flighthook/
config.rs

1use std::fmt;
2
3use flightrelay::units::{Distance, Velocity};
4use serde::{Deserialize, Serialize};
5
6use crate::game_state::Club;
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
10#[serde(rename_all = "snake_case")]
11pub enum ShotDetectionMode {
12    Full,
13    Putting,
14    Chipping,
15}
16
17impl fmt::Display for ShotDetectionMode {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::Full => write!(f, "full"),
21            Self::Putting => write!(f, "putting"),
22            Self::Chipping => write!(f, "chipping"),
23        }
24    }
25}
26
27impl From<ShotDetectionMode> for flightrelay::DetectionMode {
28    fn from(m: ShotDetectionMode) -> Self {
29        match m {
30            ShotDetectionMode::Full => Self::Full,
31            ShotDetectionMode::Putting => Self::Putting,
32            ShotDetectionMode::Chipping => Self::Chipping,
33        }
34    }
35}
36
37impl From<flightrelay::DetectionMode> for ShotDetectionMode {
38    fn from(m: flightrelay::DetectionMode) -> Self {
39        match m {
40            flightrelay::DetectionMode::Full => Self::Full,
41            flightrelay::DetectionMode::Putting => Self::Putting,
42            flightrelay::DetectionMode::Chipping => Self::Chipping,
43        }
44    }
45}
46
47// ---------------------------------------------------------------------------
48// Extension traits for flightrelay unit types (UI helpers, not protocol)
49// ---------------------------------------------------------------------------
50
51/// Flighthook-specific helpers on [`Distance`] for UI dropdowns and wire protocol shortcuts.
52pub trait DistanceExt {
53    fn unit_key(self) -> &'static str;
54    fn from_value_and_unit(value: f64, unit: &str) -> Self;
55    fn to_mm(self) -> u16;
56}
57
58impl DistanceExt for Distance {
59    fn unit_key(self) -> &'static str {
60        match self {
61            Self::Feet(_) => "feet",
62            Self::Inches(_) => "inches",
63            Self::Meters(_) => "meters",
64            Self::Centimeters(_) => "centimeters",
65            Self::Yards(_) => "yards",
66            Self::Millimeters(_) => "millimeters",
67        }
68    }
69
70    fn from_value_and_unit(value: f64, unit: &str) -> Self {
71        match unit {
72            "feet" => Self::Feet(value),
73            "meters" => Self::Meters(value),
74            "centimeters" => Self::Centimeters(value),
75            "yards" => Self::Yards(value),
76            "millimeters" => Self::Millimeters(value),
77            _ => Self::Inches(value),
78        }
79    }
80
81    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
82    fn to_mm(self) -> u16 {
83        self.as_millimeters() as u16
84    }
85}
86
87/// Flighthook-specific helpers on [`Velocity`] for UI dropdowns.
88pub trait VelocityExt {
89    fn unit_key(self) -> &'static str;
90    fn from_value_and_unit(value: f64, unit: &str) -> Self;
91}
92
93impl VelocityExt for Velocity {
94    fn unit_key(self) -> &'static str {
95        self.unit_suffix()
96    }
97
98    fn from_value_and_unit(value: f64, unit: &str) -> Self {
99        match unit {
100            "mph" => Self::MilesPerHour(value),
101            "fps" => Self::FeetPerSecond(value),
102            "kph" => Self::KilometersPerHour(value),
103            _ => Self::MetersPerSecond(value),
104        }
105    }
106}
107
108/// Unit system for display. Imperial = yards/feet/inches/mph, Metric = meters/m/s.
109#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum UnitSystem {
112    #[default]
113    Imperial,
114    Metric,
115}
116
117impl fmt::Display for UnitSystem {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::Imperial => write!(f, "imperial"),
121            Self::Metric => write!(f, "metric"),
122        }
123    }
124}
125
126// ---------------------------------------------------------------------------
127// Club-to-mode mapping defaults
128// ---------------------------------------------------------------------------
129
130pub fn default_chipping_clubs() -> Vec<Club> {
131    vec![Club::GapWedge, Club::SandWedge, Club::LobWedge]
132}
133
134pub fn default_putting_clubs() -> Vec<Club> {
135    vec![Club::Putter]
136}
137
138// ---------------------------------------------------------------------------
139// Persisted config types (shared between app and UI)
140// ---------------------------------------------------------------------------
141
142/// Top-level persisted config. All fields are in user-friendly units
143/// (inches, feet, 0-100 percent) so the TOML file is hand-editable.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct FlighthookConfig {
146    /// Default unit system for shot display (freedom units by default)
147    #[serde(default)]
148    pub default_units: UnitSystem,
149    /// Clubs that trigger Chipping mode on selection.
150    #[serde(default = "default_chipping_clubs")]
151    pub chipping_clubs: Vec<Club>,
152    /// Clubs that trigger Putting mode on selection.
153    #[serde(default = "default_putting_clubs")]
154    pub putting_clubs: Vec<Club>,
155    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
156    pub webserver: std::collections::HashMap<String, WebserverSection>,
157    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
158    pub mevo: std::collections::HashMap<String, MevoSection>,
159    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
160    pub r10: std::collections::HashMap<String, R10Section>,
161    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
162    pub mock_monitor: std::collections::HashMap<String, MockMonitorSection>,
163    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
164    pub gspro: std::collections::HashMap<String, GsProSection>,
165    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
166    pub random_club: std::collections::HashMap<String, RandomClubSection>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170pub struct WebserverSection {
171    #[serde(default)]
172    pub name: String,
173    pub bind: String,
174}
175
176/// A Mevo/Mevo+ device instance.
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178pub struct MevoSection {
179    #[serde(default)]
180    pub name: String,
181    pub address: Option<String>,
182    pub ball_type: Option<u8>,
183    pub tee_height: Option<Distance>,
184    pub range: Option<Distance>,
185    pub surface_height: Option<Distance>,
186    pub track_pct: Option<f64>,
187    /// Whether to use estimated (E8 fallback) shots. Defaults to true when
188    /// absent for backwards compatibility. Estimated shots may lack sidespin
189    /// and carry less data, but are often the only result for short chips.
190    #[serde(default)]
191    pub use_estimated: Option<bool>,
192}
193
194/// A Garmin R10 BLE device instance.
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct R10Section {
197    #[serde(default)]
198    pub name: String,
199}
200
201/// A mock launch monitor instance.
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct MockMonitorSection {
204    #[serde(default)]
205    pub name: String,
206}
207
208/// A GSPro integration instance.
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210pub struct GsProSection {
211    #[serde(default)]
212    pub name: String,
213    pub address: Option<String>,
214    /// Actor ID for full-swing shots (e.g. "mevo.0"). None = accept from any monitor.
215    #[serde(default)]
216    pub full_monitor: Option<String>,
217    /// Actor ID for chipping shots. None = accept from any monitor.
218    #[serde(default)]
219    pub chipping_monitor: Option<String>,
220    /// Actor ID for putting shots. None = accept from any monitor.
221    #[serde(default)]
222    pub putting_monitor: Option<String>,
223}
224
225/// A random club cycling integration instance.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct RandomClubSection {
228    #[serde(default)]
229    pub name: String,
230}
231
232impl FlighthookConfig {
233    /// Look up the detection mode for a club based on the configured mapping.
234    ///
235    /// Clubs in `putting_clubs` → Putting, in `chipping_clubs` → Chipping,
236    /// everything else → Full.
237    pub fn club_mode(&self, club: Club) -> ShotDetectionMode {
238        if self.putting_clubs.contains(&club) {
239            ShotDetectionMode::Putting
240        } else if self.chipping_clubs.contains(&club) {
241            ShotDetectionMode::Chipping
242        } else {
243            ShotDetectionMode::Full
244        }
245    }
246
247    /// Returns true if any user-configured actors (devices or integrations)
248    /// exist. Webservers are infrastructure and don't count.
249    pub fn has_user_actors(&self) -> bool {
250        !self.mevo.is_empty()
251            || !self.r10.is_empty()
252            || !self.mock_monitor.is_empty()
253            || !self.gspro.is_empty()
254            || !self.random_club.is_empty()
255    }
256}
257
258impl Default for FlighthookConfig {
259    /// Minimal empty config — webserver only, no devices or integrations.
260    /// The setup wizard will add devices on first startup.
261    fn default() -> Self {
262        let mut webserver = std::collections::HashMap::new();
263        webserver.insert(
264            "0".into(),
265            WebserverSection {
266                name: "Web Server".into(),
267                bind: "0.0.0.0:5880".into(),
268            },
269        );
270        Self {
271            default_units: UnitSystem::default(),
272            chipping_clubs: default_chipping_clubs(),
273            putting_clubs: default_putting_clubs(),
274            webserver,
275            mevo: std::collections::HashMap::new(),
276            r10: std::collections::HashMap::new(),
277            mock_monitor: std::collections::HashMap::new(),
278            gspro: std::collections::HashMap::new(),
279            random_club: std::collections::HashMap::new(),
280        }
281    }
282}
283
284impl Default for MevoSection {
285    fn default() -> Self {
286        Self {
287            name: "Mevo WiFi".into(),
288            address: Some("192.168.2.1:5100".into()),
289            ball_type: Some(0),
290            tee_height: Some(Distance::Inches(1.5)),
291            range: Some(Distance::Feet(8.0)),
292            surface_height: Some(Distance::Inches(0.0)),
293            track_pct: Some(80.0),
294            use_estimated: None,
295        }
296    }
297}
298
299impl Default for R10Section {
300    fn default() -> Self {
301        Self {
302            name: "Garmin R10".into(),
303        }
304    }
305}
306
307impl Default for GsProSection {
308    fn default() -> Self {
309        Self {
310            name: "Local GSPro".into(),
311            address: Some("127.0.0.1:921".into()),
312            full_monitor: None,
313            chipping_monitor: None,
314            putting_monitor: None,
315        }
316    }
317}