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>
impl<S: Read + Write> BinaryConnection<S>
Sourcepub fn stream_mut(&mut self) -> &mut S
pub fn stream_mut(&mut self) -> &mut S
Mutably borrow the underlying stream (e.g. to set read timeout).
Examples found in repository?
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}Sourcepub fn set_on_send(&mut self, f: impl FnMut(&Command, BusAddr) + 'static)
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?
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}Sourcepub fn set_on_recv(&mut self, f: impl FnMut(&Envelope) + 'static)
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?
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}Sourcepub fn send(&mut self, cmd: &Command, dest: BusAddr) -> Result<(), ConnError>
pub fn send(&mut self, cmd: &Command, dest: BusAddr) -> Result<(), ConnError>
Send a command to the given bus address.
Sourcepub fn send_raw(&mut self, frame: &RawFrame) -> Result<(), ConnError>
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()).
Sourcepub fn recv(&mut self) -> Result<Option<Envelope>, ConnError>
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/TimedOutfrom 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?
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>
impl BinaryConnection<TcpStream>
Sourcepub fn connect(addr: impl ToSocketAddrs) -> Result<Self, ConnError>
pub fn connect(addr: impl ToSocketAddrs) -> Result<Self, ConnError>
Connect to a Mevo+ device with the system default timeout.
Sourcepub fn connect_timeout(
addr: &SocketAddr,
timeout: Duration,
) -> Result<Self, ConnError>
pub fn connect_timeout( addr: &SocketAddr, timeout: Duration, ) -> Result<Self, ConnError>
Connect with an explicit timeout.
Examples found in repository?
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}Sourcepub fn peer_addr(&self) -> Result<SocketAddr, ConnError>
pub fn peer_addr(&self) -> Result<SocketAddr, ConnError>
The peer address of the underlying TCP connection.
Sourcepub fn recv_timeout(&mut self, timeout: Duration) -> Result<Envelope, ConnError>
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.