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
47pub 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
87pub 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#[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
126pub 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct FlighthookConfig {
146 #[serde(default)]
148 pub default_units: UnitSystem,
149 #[serde(default = "default_chipping_clubs")]
151 pub chipping_clubs: Vec<Club>,
152 #[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#[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 #[serde(default)]
191 pub use_estimated: Option<bool>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct R10Section {
197 #[serde(default)]
198 pub name: String,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct MockMonitorSection {
204 #[serde(default)]
205 pub name: String,
206}
207
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210pub struct GsProSection {
211 #[serde(default)]
212 pub name: String,
213 pub address: Option<String>,
214 #[serde(default)]
216 pub full_monitor: Option<String>,
217 #[serde(default)]
219 pub chipping_monitor: Option<String>,
220 #[serde(default)]
222 pub putting_monitor: Option<String>,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct RandomClubSection {
228 #[serde(default)]
229 pub name: String,
230}
231
232impl FlighthookConfig {
233 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 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 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}