Skip to main content

BinaryConnection

Struct BinaryConnection 

Source
pub struct BinaryConnection<S: Read + Write> { /* private fields */ }
Expand description

Binary protocol connection to a Mevo+ device.

Generic over S: Read + Write so callers can use any stream type. For non-blocking usage, configure the stream’s read timeout externally and call recv() — it returns Ok(None) on WouldBlock/TimedOut instead of blocking.

§Example (TcpStream)

use std::time::Duration;
use ironsight::{BinaryConnection, ConnError};

let mut conn = BinaryConnection::connect(ironsight::conn::DEFAULT_ADDR)?;
conn.stream_mut().set_read_timeout(Some(Duration::from_millis(100)))?;
loop {
    match conn.recv()? {
        Some(env) => println!("{:?}", env.message),
        None => { /* send keepalive */ }
    }
}

Implementations§

Source§

impl<S: Read + Write> BinaryConnection<S>

Source

pub fn new(stream: S) -> Self

Wrap any Read + Write stream as a binary protocol connection.

Source

pub fn stream(&self) -> &S

Borrow the underlying stream (e.g. to query peer address).

Source

pub fn stream_mut(&mut self) -> &mut S

Mutably borrow the underlying stream (e.g. to set read timeout).

Examples found in repository?
examples/event_loop.rs (line 183)
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}
Source

pub fn set_on_send(&mut self, f: impl FnMut(&Command, BusAddr) + 'static)

Register a callback invoked at the top of every send() call.

Examples found in repository?
examples/event_loop.rs (line 157)
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}
Source

pub fn set_on_recv(&mut self, f: impl FnMut(&Envelope) + 'static)

Register a callback invoked after every successful frame decode.

Examples found in repository?
examples/event_loop.rs (line 158)
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}
Source

pub fn send(&mut self, cmd: &Command, dest: BusAddr) -> Result<(), ConnError>

Send a command to the given bus address.

Source

pub fn send_raw(&mut self, frame: &RawFrame) -> Result<(), ConnError>

Send a pre-built raw frame (for messages not in Command, e.g. ClubPrc::encode_request()).

Source

pub fn recv(&mut self) -> Result<Option<Envelope>, ConnError>

Try to receive the next complete frame.

  • Ok(Some(env)) — decoded message.
  • Ok(None) — no data available (WouldBlock/TimedOut from stream).
  • Err(Disconnected) — stream closed by peer.
  • Err(Wire|Io) — decode or I/O error.

The stream’s read timeout controls whether this blocks or returns Ok(None) immediately. Set the timeout on the stream before calling.

Examples found in repository?
examples/event_loop.rs (line 189)
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}
Source§

impl BinaryConnection<TcpStream>

Source

pub fn connect(addr: impl ToSocketAddrs) -> Result<Self, ConnError>

Connect to a Mevo+ device with the system default timeout.

Source

pub fn connect_timeout( addr: &SocketAddr, timeout: Duration, ) -> Result<Self, ConnError>

Connect with an explicit timeout.

Examples found in repository?
examples/event_loop.rs (line 153)
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}
Source

pub fn peer_addr(&self) -> Result<SocketAddr, ConnError>

The peer address of the underlying TCP connection.

Source

pub fn shutdown(&self) -> Result<(), ConnError>

Shut down the TCP connection.

Source

pub fn recv_timeout(&mut self, timeout: Duration) -> Result<Envelope, ConnError>

Block up to timeout for a complete frame.

Returns ConnError::Timeout if no complete frame arrives in time.

This is a convenience wrapper for callers that prefer the old timeout-per-call style. Prefer setting the stream read timeout once and using recv() directly.

Auto Trait Implementations§

§

impl<S> Freeze for BinaryConnection<S>
where S: Freeze,

§

impl<S> !RefUnwindSafe for BinaryConnection<S>

§

impl<S> !Send for BinaryConnection<S>

§

impl<S> !Sync for BinaryConnection<S>

§

impl<S> Unpin for BinaryConnection<S>
where S: Unpin,

§

impl<S> UnsafeUnpin for BinaryConnection<S>
where S: UnsafeUnpin,

§

impl<S> !UnwindSafe for BinaryConnection<S>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.