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