Skip to main content

event_loop/
event_loop.rs

1//! Main processing loop: connect to a live Mevo+ and print all messages.
2//!
3//! Usage: cargo run --example event_loop
4//!
5//! Requires: connected to Mevo+ WiFi (SSID = device serial).
6
7use std::net::SocketAddr;
8use std::process;
9use std::time::{Duration, Instant};
10
11use ironsight::conn::DEFAULT_ADDR;
12use ironsight::protocol::camera::CamConfig;
13use ironsight::protocol::config::{MODE_OUTDOOR, ParamData, ParamValue, RadarCal};
14use ironsight::seq::{
15    self, Action, AvrSettings, AvrSync, DspSync, PiSync, ShotSequencer,
16};
17use ironsight::{BinaryConnection, ConnError, Message, Sequence};
18
19// ---------------------------------------------------------------------------
20// Unit conversion helpers
21// ---------------------------------------------------------------------------
22
23/// m/s → mph
24fn ms_to_mph(ms: f64) -> f64 {
25    ms * 2.23694
26}
27
28/// meters → yards
29fn m_to_yd(m: f64) -> f64 {
30    m * 1.09361
31}
32
33/// meters → feet
34fn m_to_ft(m: f64) -> f64 {
35    m * 3.28084
36}
37
38/// meters → inches
39fn m_to_in(m: f64) -> f64 {
40    m * 39.3701
41}
42
43// ---------------------------------------------------------------------------
44// Default settings
45// ---------------------------------------------------------------------------
46
47fn default_avr_settings() -> AvrSettings {
48    AvrSettings {
49        mode: MODE_OUTDOOR,
50        params: vec![
51            // Ball type: 0 = RCT
52            ParamValue {
53                param_id: 0x06,
54                value: ParamData::Int24(0),
55            },
56            // Outdoor minimum track %: 1.0
57            ParamValue {
58                param_id: 0x0F,
59                value: ParamData::Float40(1.0),
60            },
61            // Driver tee height: 1.5 inches = 0.0381m
62            ParamValue {
63                param_id: 0x26,
64                value: ParamData::Float40(0.0381),
65            },
66        ],
67        radar_cal: RadarCal {
68            range_mm: 2743, // 9 feet
69            height_mm: 25,  // floor(1.0 inches * 25.4) = 25
70        },
71    }
72}
73
74fn default_cam_config() -> CamConfig {
75    CamConfig {
76        dynamic_config: true,
77        resolution_width: 1024,
78        resolution_height: 768,
79        rotation: 0,
80        ev: 0,
81        quality: 80,
82        framerate: 20,
83        streaming_framerate: 1,
84        ringbuffer_pretime_ms: 1000,
85        ringbuffer_posttime_ms: 4000,
86        raw_camera_mode: 0,
87        fusion_camera_mode: false,
88        raw_shutter_speed_max: 0.0,
89        raw_ev_roi_x: -1,
90        raw_ev_roi_y: -1,
91        raw_ev_roi_width: -1,
92        raw_ev_roi_height: -1,
93        raw_x_offset: -1,
94        raw_bin44: false,
95        raw_live_preview_write_interval_ms: -1,
96        raw_y_offset: -1,
97        buffer_sub_sampling_pre_trigger_div: -1,
98        buffer_sub_sampling_post_trigger_div: -1,
99        buffer_sub_sampling_switch_time_offset: -1.0,
100        buffer_sub_sampling_total_buffer_size: -1,
101        buffer_sub_sampling_pre_trigger_buffer_size: -1,
102    }
103}
104
105// ---------------------------------------------------------------------------
106// Pretty-printers
107// ---------------------------------------------------------------------------
108
109fn print_handshake(dsp: &DspSync, avr: &AvrSync, pi: &PiSync) {
110    println!("--- Handshake complete ---");
111    println!(
112        "  DSP: type=0x{:02X} pcb={}",
113        dsp.hw_info.dsp_type, dsp.hw_info.pcb
114    );
115    println!("  DSP info: {}", dsp.dev_info.text);
116    println!("  AVR info: {}", avr.dev_info.text);
117    println!(
118        "  AVR tilt={:.1}° roll={:.1}°",
119        avr.status.tilt, -avr.status.roll
120    );
121    println!("  PI info: {}", pi.dev_info.text);
122    println!("  SSID: {}  password: {}", pi.ssid, pi.password);
123    println!(
124        "  Battery: {}%  {}",
125        dsp.status.battery_percent(),
126        if dsp.status.external_power() {
127            "(plugged in)"
128        } else {
129            ""
130        },
131    );
132}
133
134// ---------------------------------------------------------------------------
135// Main
136// ---------------------------------------------------------------------------
137
138fn main() {
139    if let Err(e) = run() {
140        eprintln!("error: {e}");
141        process::exit(1);
142    }
143}
144
145fn send_action(conn: &mut BinaryConnection<std::net::TcpStream>, action: Action) -> Result<(), ConnError> {
146    seq::send_action(conn, action)
147}
148
149fn run() -> Result<(), ConnError> {
150    // 1. Connect
151    let addr: SocketAddr = DEFAULT_ADDR.parse().unwrap();
152    println!("Connecting to {addr}...");
153    let mut conn = BinaryConnection::connect_timeout(&addr, Duration::from_secs(5))?;
154    println!("Connected.");
155
156    // Wire-level trace callbacks.
157    conn.set_on_send(|cmd, dest| println!("\n>> {:?} [{}]", cmd, cmd.debug_hex(dest)));
158    conn.set_on_recv(|env| println!("\n<< {env:?}"));
159
160    // 2. Handshake (blocking via drive())
161    println!("DSP sync...");
162    let dsp = seq::sync_dsp(&mut conn)?;
163    println!("AVR sync...");
164    let avr = seq::sync_avr(&mut conn)?;
165    println!("PI sync...");
166    let pi = seq::sync_pi(&mut conn)?;
167    print_handshake(&dsp, &avr, &pi);
168
169    // 3. Configure
170    let settings = default_avr_settings();
171    println!(
172        "Configuring... mode=Outdoor ball=RCT range={}mm height={}mm",
173        settings.radar_cal.range_mm, settings.radar_cal.height_mm,
174    );
175    seq::configure_avr(&mut conn, &settings)?;
176    seq::configure_camera(&mut conn, &default_cam_config())?;
177
178    // 4. Arm
179    seq::arm(&mut conn)?;
180    println!("=== ARMED — waiting for shots ===");
181
182    // 5. Non-blocking event loop
183    conn.stream_mut()
184        .set_read_timeout(Some(Duration::from_millis(1)))?;
185    let mut last_keepalive = Instant::now();
186    let mut shot: Option<ShotSequencer> = None;
187
188    loop {
189        if let Some(env) = conn.recv()? {
190            // If a shot sequence is active, feed it first.
191            if let Some(ref mut s) = shot {
192                let actions = s.feed(&env);
193                for a in actions {
194                    send_action(&mut conn, a)?;
195                }
196                if s.is_complete() {
197                    let data = shot.take().unwrap().into_result();
198                    print_shot(&data);
199                    continue;
200                }
201            }
202
203            // Handle messages not consumed by the shot sequencer.
204            match &env.message {
205                Message::ShotText(st) if st.is_processed() && shot.is_none() => {
206                    println!("  Shot processed — starting shot sequence...");
207                    let (s, actions) = ShotSequencer::new();
208                    for a in actions {
209                        send_action(&mut conn, a)?;
210                    }
211                    shot = Some(s);
212                }
213
214                // --- Flight result (primary) ---
215                Message::FlightResult(r) => {
216                    println!("  Flight result (shot #{}):", r.total);
217                    println!(
218                        "    Ball speed: {:.1} mph  VLA: {:.1}°  HLA: {:.1}°",
219                        ms_to_mph(r.launch_speed),
220                        r.launch_elevation,
221                        r.launch_azimuth,
222                    );
223                    println!(
224                        "    Carry: {:.1} yd  Total: {:.1} yd  Height: {:.1} ft",
225                        m_to_yd(r.carry_distance),
226                        m_to_yd(r.total_distance),
227                        m_to_ft(r.max_height),
228                    );
229                    println!(
230                        "    Backspin: {} rpm  Sidespin: {} rpm",
231                        r.backspin_rpm, r.sidespin_rpm,
232                    );
233                    println!(
234                        "    Club speed: {:.1} mph  Path: {:.1}°  AoA: {:.1}°",
235                        ms_to_mph(r.clubhead_speed),
236                        r.club_strike_direction,
237                        r.club_attack_angle,
238                    );
239                    println!(
240                        "    Face: {:.1}°  Loft: {:.1}°",
241                        r.club_face_angle, r.club_effective_loft,
242                    );
243                }
244
245                // --- Flight result v1 (early) ---
246                Message::FlightResultV1(r) => {
247                    println!("  Flight result v1 (shot #{}):", r.total);
248                    println!(
249                        "    Ball: {:.1} mph  Club: {:.1} mph  VLA: {:.1}°  HLA: {:.1}°",
250                        ms_to_mph(r.ball_velocity),
251                        ms_to_mph(r.club_velocity),
252                        r.elevation,
253                        r.azimuth,
254                    );
255                    println!(
256                        "    Dist: {:.1} yd  Height: {:.1} ft  Backspin: {} rpm",
257                        m_to_yd(r.distance),
258                        m_to_ft(r.height),
259                        r.backspin_rpm,
260                    );
261                }
262
263                // --- Club result ---
264                Message::ClubResult(r) => {
265                    println!("  Club result:");
266                    println!(
267                        "    Club speed: {:.1} mph (pre) / {:.1} mph (post)  Smash: {:.2}",
268                        ms_to_mph(r.pre_club_speed),
269                        ms_to_mph(r.post_club_speed),
270                        r.smash_factor,
271                    );
272                    println!(
273                        "    Path: {:.1}°  Face: {:.1}°  AoA: {:.1}°  Loft: {:.1}°",
274                        r.strike_direction, r.face_angle, r.attack_angle, r.dynamic_loft,
275                    );
276                    println!(
277                        "    Low point: {:.1} in  Club height: {:.1} in",
278                        m_to_in(r.club_offset),
279                        m_to_in(r.club_height),
280                    );
281                }
282
283                // --- Spin result ---
284                Message::SpinResult(r) => {
285                    println!("  Spin result:");
286                    println!(
287                        "    Total spin: {} rpm (PM final)  Axis: {:.1}°  Method: {}",
288                        r.pm_spin_final, r.spin_axis, r.spin_method,
289                    );
290                    println!(
291                        "    AM: {} rpm  PM: {} rpm  Launch: {} rpm  AOD: {} rpm  PLL: {} rpm",
292                        r.am_spin, r.pm_spin, r.launch_spin, r.aod_spin, r.pll_spin,
293                    );
294                }
295
296                // --- Speed profile ---
297                Message::SpeedProfile(r) => {
298                    let peak = r.speeds.iter().copied().fold(0.0_f64, f64::max);
299                    println!(
300                        "  Speed profile: {} samples ({}+{})  peak: {:.1} mph",
301                        r.speeds.len(),
302                        r.num_pre,
303                        r.num_post,
304                        ms_to_mph(peak),
305                    );
306                }
307
308                // --- Tracking status ---
309                Message::TrackingStatus(r) => {
310                    println!(
311                        "  Tracking: iter={} quality={} prc_count={} trigger_idx={}",
312                        r.processing_iteration,
313                        r.result_quality,
314                        r.prc_tracking_count,
315                        r.trigger_idx,
316                    );
317                }
318
319                // --- PRC data ---
320                Message::PrcData(r) => {
321                    println!("  PRC: seq={} points={}", r.sequence, r.points.len());
322                }
323
324                // --- Club PRC data ---
325                Message::ClubPrc(r) => {
326                    println!("  Club PRC: points={}", r.points.len());
327                }
328
329                // --- Camera image available ---
330                Message::CamImageAvail(r) => {
331                    println!(
332                        "  Camera: streaming={} fusion={} video={}",
333                        r.streaming_available, r.fusion_available, r.video_available,
334                    );
335                }
336
337                // --- DspStatus from keepalive ---
338                Message::DspStatus(s) => {
339                    println!(
340                        "[keepalive] battery={}%{}  temp={:.1}°C",
341                        s.battery_percent(),
342                        if s.external_power() {
343                            " (plugged in)"
344                        } else {
345                            ""
346                        },
347                        s.temperature_c(),
348                    );
349                }
350                Message::AvrStatus(s) => {
351                    println!("[keepalive] tilt={:.1}° roll={:.1}°", s.tilt, -s.roll);
352                }
353
354                // --- Config ack / noise ---
355                Message::ConfigAck(_) | Message::PiStatus(_) => {}
356
357                // --- Everything else: debug line is enough ---
358                _ => {}
359            }
360        }
361
362        // Heartbeats always fire — even during shot sequencing.
363        if last_keepalive.elapsed() >= Duration::from_secs(1) {
364            for a in seq::keepalive_actions() {
365                send_action(&mut conn, a)?;
366            }
367            last_keepalive = Instant::now();
368        }
369    }
370}
371
372fn print_shot(data: &seq::ShotData) {
373    println!("\n  === Shot complete ===");
374    if let Some(ref f) = data.flight {
375        println!(
376            "    Ball: {:.1} mph  Carry: {:.0} yd",
377            ms_to_mph(f.launch_speed),
378            m_to_yd(f.carry_distance),
379        );
380    }
381    if let Some(ref c) = data.club {
382        println!(
383            "    Club: {:.1} mph  Smash: {:.2}",
384            ms_to_mph(c.pre_club_speed),
385            c.smash_factor,
386        );
387    }
388    println!("  === RE-ARMED ===\n");
389}