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?
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}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?
33fn run() -> Result<(), ironsight::ConnError> {
34 let addr: SocketAddr = DEFAULT_ADDR.parse().unwrap();
35 println!("Connecting to {addr}...");
36 let mut conn = BinaryConnection::connect_timeout(&addr, Duration::from_secs(5))?;
37 println!("Connected.");
38
39 conn.set_on_send(|cmd, dest| println!(">> {}", cmd.debug_hex(dest)));
40 conn.set_on_recv(|env| println!("<< {env:?}"));
41
42 let mut client = BinaryClient::from_tcp(conn)?;
43
44 // Enqueue the full startup sequence.
45 client.handshake();
46 client.configure_avr(default_avr_settings());
47 client.configure_cam(default_cam_config());
48 client.arm();
49
50 println!("Starting poll loop...");
51 loop {
52 if let Some(event) = client.poll()? {
53 match event {
54 BinaryEvent::Handshake(h) => {
55 println!("\n=== Handshake complete ===");
56 println!(" SSID: {} password: {}", h.pi.ssid, h.pi.password);
57 println!(" DSP: {}", h.dsp.dev_info.text);
58 println!(" AVR: {}", h.avr.dev_info.text);
59 println!(" PI: {}", h.pi.dev_info.text);
60 println!(
61 " Battery: {}%{}",
62 h.dsp.status.battery_percent(),
63 if h.dsp.status.external_power() {
64 " (plugged in)"
65 } else {
66 ""
67 },
68 );
69 }
70 BinaryEvent::Configured => {
71 println!("\n=== Configured ===");
72 }
73 BinaryEvent::Armed => {
74 println!("\n=== ARMED — waiting for shots ===");
75 }
76 BinaryEvent::Trigger => {
77 println!("\n ** BALL TRIGGER **");
78 }
79 BinaryEvent::ShotComplete(data) => {
80 println!("\n === Shot complete ===");
81 if let Some(ref f) = data.flight {
82 println!(
83 " Ball: {:.1} mph Carry: {:.0} yd VLA: {:.1}° HLA: {:.1}°",
84 ms_to_mph(f.launch_speed),
85 m_to_yd(f.carry_distance),
86 f.launch_elevation,
87 f.launch_azimuth,
88 );
89 }
90 if let Some(ref c) = data.club {
91 println!(
92 " Club: {:.1} mph Smash: {:.2} Path: {:.1}° Face: {:.1}°",
93 ms_to_mph(c.pre_club_speed),
94 c.smash_factor,
95 c.strike_direction,
96 c.face_angle,
97 );
98 }
99 if let Some(ref s) = data.spin {
100 println!(
101 " Spin: {} rpm Axis: {:.1}°",
102 s.pm_spin_final, s.spin_axis,
103 );
104 }
105 println!(" === RE-ARMED ===");
106 }
107 BinaryEvent::Disarmed => {
108 println!("\n=== Disarmed ===");
109 }
110 BinaryEvent::ShotDatum(_)
111 | BinaryEvent::Keepalive(_)
112 | BinaryEvent::Message(_) => {
113 // on_recv callback already printed it
114 }
115 }
116 }
117 }
118}More examples
47fn run() -> Result<(), ironsight::ConnError> {
48 let addr: SocketAddr = DEFAULT_ADDR.parse().unwrap();
49 println!("=== Mode Change Integration Test ===\n");
50
51 // ── Step 1: Connect ──────────────────────────────────────────────────
52 println!("[connect] connecting to {addr}...");
53 let mut conn = BinaryConnection::connect_timeout(&addr, Duration::from_secs(5))?;
54 println!("[connect] OK\n");
55
56 conn.set_on_send(|cmd, dest| {
57 eprintln!(" >> {dest:?} {}", cmd.debug_hex(dest));
58 });
59 conn.set_on_recv(|env| {
60 let hex: String = env.raw.iter().map(|b| format!("{b:02X}")).collect();
61 eprintln!(" << 0x{:02X} {hex} | {:?}", env.type_id, env.message);
62 });
63
64 let mut client = BinaryClient::from_tcp(conn)?;
65 client.set_keepalive_interval(Duration::from_secs(3));
66
67 // Enqueue: handshake → configure(outdoor) → cam → arm
68 client.handshake();
69 client.configure_avr(avr_settings_full(MODE_OUTDOOR));
70 client.configure_cam(cam_config());
71 client.arm();
72
73 let mut step = Step::InitialArm;
74 let mut phase_start = Instant::now();
75 let timeout = Duration::from_secs(60);
76
77 // Track sub-events within InitialArm
78 let mut handshake_done = false;
79 let mut configure_count = 0u8; // need 2: AVR + cam
80
81 println!("[step] InitialArm: handshake → configure(outdoor/9) → cam → arm");
82
83 loop {
84 if phase_start.elapsed() > timeout {
85 return Err(ironsight::ConnError::Protocol(format!(
86 "timeout in step {step:?} after {timeout:?}"
87 )));
88 }
89
90 let event = client.poll()?;
91 let Some(event) = event else {
92 continue;
93 };
94
95 match (&step, &event) {
96 // ── InitialArm phase ──────────────────────────────────────
97 (Step::InitialArm, BinaryEvent::Handshake(h)) => {
98 println!(
99 " [handshake] OK — {} | battery {}%",
100 h.avr.dev_info.text.trim(),
101 h.dsp.status.battery_percent()
102 );
103 handshake_done = true;
104 }
105 (Step::InitialArm, BinaryEvent::Configured) => {
106 configure_count += 1;
107 let label = if configure_count == 1 { "AVR" } else { "cam" };
108 println!(" [configured] {label} OK");
109 }
110 (Step::InitialArm, BinaryEvent::Armed) => {
111 println!(" [armed] OK — device armed in outdoor mode");
112 assert!(handshake_done, "armed before handshake");
113 assert!(configure_count >= 2, "armed before both configures");
114
115 // Transition: wait for keepalive
116 step = Step::WaitKeepalive;
117 phase_start = Instant::now();
118 println!("\n[step] WaitKeepalive: waiting for keepalive...");
119 }
120
121 // ── WaitKeepalive ─────────────────────────────────────────
122 (Step::WaitKeepalive, BinaryEvent::Keepalive(snap)) => {
123 let avr = snap.avr.as_ref().map(|a| format!("tilt={:.1}", a.tilt));
124 let dsp = snap
125 .dsp
126 .as_ref()
127 .map(|d| format!("bat={}%", d.battery_percent()));
128 println!(" [keepalive] OK — {} {}", dsp.unwrap_or_default(), avr.unwrap_or_default());
129
130 // Transition: first mode change outdoor → chipping
131 step = Step::ModeChange1;
132 phase_start = Instant::now();
133 println!("\n[step] ModeChange1: outdoor(9) → chipping(5)");
134 client.configure_avr(avr_settings_mode_only(MODE_CHIPPING));
135 client.arm();
136 }
137
138 // ── ModeChange1 (outdoor → chipping) ─────────────────────
139 (Step::ModeChange1, BinaryEvent::Disarmed) => {
140 println!(" [disarmed] OK");
141 }
142 (Step::ModeChange1, BinaryEvent::Configured) => {
143 println!(" [configured] chipping OK");
144 }
145 (Step::ModeChange1, BinaryEvent::Armed) => {
146 println!(" [armed] OK — chipping mode");
147
148 step = Step::WaitKeepalive2;
149 phase_start = Instant::now();
150 println!("\n[step] WaitKeepalive2: waiting for keepalive...");
151 }
152
153 // ── WaitKeepalive2 ────────────────────────────────────────
154 (Step::WaitKeepalive2, BinaryEvent::Keepalive(snap)) => {
155 let dsp = snap
156 .dsp
157 .as_ref()
158 .map(|d| format!("bat={}%", d.battery_percent()));
159 println!(" [keepalive] OK — {}", dsp.unwrap_or_default());
160
161 // Transition: second mode change chipping → putting
162 step = Step::ModeChange2;
163 phase_start = Instant::now();
164 println!("\n[step] ModeChange2: chipping(5) → putting(3)");
165 client.configure_avr(avr_settings_mode_only(MODE_PUTTING));
166 client.arm();
167 }
168
169 // ── ModeChange2 (chipping → putting) ─────────────────────
170 (Step::ModeChange2, BinaryEvent::Disarmed) => {
171 println!(" [disarmed] OK");
172 }
173 (Step::ModeChange2, BinaryEvent::Configured) => {
174 println!(" [configured] putting OK");
175 }
176 (Step::ModeChange2, BinaryEvent::Armed) => {
177 println!(" [armed] OK — putting mode");
178
179 step = Step::WaitKeepalive3;
180 phase_start = Instant::now();
181 println!("\n[step] WaitKeepalive3: waiting for keepalive...");
182 }
183
184 // ── WaitKeepalive3 ────────────────────────────────────────
185 (Step::WaitKeepalive3, BinaryEvent::Keepalive(snap)) => {
186 let dsp = snap
187 .dsp
188 .as_ref()
189 .map(|d| format!("bat={}%", d.battery_percent()));
190 println!(" [keepalive] OK — {}", dsp.unwrap_or_default());
191
192 step = Step::Done;
193 }
194
195 // ── Done ──────────────────────────────────────────────────
196 (Step::Done, _) => unreachable!(),
197
198 // ── Passthrough (text, messages, etc.) ────────────────────
199 (_, BinaryEvent::Message(_)) => {}
200 (_, BinaryEvent::Keepalive(_)) => {} // extra keepalives
201 (_, other) => {
202 println!(" [unexpected] {other:?} in step {step:?}");
203 }
204 }
205
206 if step == Step::Done {
207 break;
208 }
209 }
210
211 println!("\n=== ALL STEPS PASSED ===");
212 println!(" 1. Connect + handshake + configure(outdoor) + arm ✓");
213 println!(" 2. Keepalive received while armed ✓");
214 println!(" 3. Mode change outdoor → chipping ✓");
215 println!(" 4. Keepalive after mode change ✓");
216 println!(" 5. Mode change chipping → putting ✓");
217 println!(" 6. Keepalive after second mode change ✓");
218
219 Ok(())
220}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}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?
33fn run() -> Result<(), ironsight::ConnError> {
34 let addr: SocketAddr = DEFAULT_ADDR.parse().unwrap();
35 println!("Connecting to {addr}...");
36 let mut conn = BinaryConnection::connect_timeout(&addr, Duration::from_secs(5))?;
37 println!("Connected.");
38
39 conn.set_on_send(|cmd, dest| println!(">> {}", cmd.debug_hex(dest)));
40 conn.set_on_recv(|env| println!("<< {env:?}"));
41
42 let mut client = BinaryClient::from_tcp(conn)?;
43
44 // Enqueue the full startup sequence.
45 client.handshake();
46 client.configure_avr(default_avr_settings());
47 client.configure_cam(default_cam_config());
48 client.arm();
49
50 println!("Starting poll loop...");
51 loop {
52 if let Some(event) = client.poll()? {
53 match event {
54 BinaryEvent::Handshake(h) => {
55 println!("\n=== Handshake complete ===");
56 println!(" SSID: {} password: {}", h.pi.ssid, h.pi.password);
57 println!(" DSP: {}", h.dsp.dev_info.text);
58 println!(" AVR: {}", h.avr.dev_info.text);
59 println!(" PI: {}", h.pi.dev_info.text);
60 println!(
61 " Battery: {}%{}",
62 h.dsp.status.battery_percent(),
63 if h.dsp.status.external_power() {
64 " (plugged in)"
65 } else {
66 ""
67 },
68 );
69 }
70 BinaryEvent::Configured => {
71 println!("\n=== Configured ===");
72 }
73 BinaryEvent::Armed => {
74 println!("\n=== ARMED — waiting for shots ===");
75 }
76 BinaryEvent::Trigger => {
77 println!("\n ** BALL TRIGGER **");
78 }
79 BinaryEvent::ShotComplete(data) => {
80 println!("\n === Shot complete ===");
81 if let Some(ref f) = data.flight {
82 println!(
83 " Ball: {:.1} mph Carry: {:.0} yd VLA: {:.1}° HLA: {:.1}°",
84 ms_to_mph(f.launch_speed),
85 m_to_yd(f.carry_distance),
86 f.launch_elevation,
87 f.launch_azimuth,
88 );
89 }
90 if let Some(ref c) = data.club {
91 println!(
92 " Club: {:.1} mph Smash: {:.2} Path: {:.1}° Face: {:.1}°",
93 ms_to_mph(c.pre_club_speed),
94 c.smash_factor,
95 c.strike_direction,
96 c.face_angle,
97 );
98 }
99 if let Some(ref s) = data.spin {
100 println!(
101 " Spin: {} rpm Axis: {:.1}°",
102 s.pm_spin_final, s.spin_axis,
103 );
104 }
105 println!(" === RE-ARMED ===");
106 }
107 BinaryEvent::Disarmed => {
108 println!("\n=== Disarmed ===");
109 }
110 BinaryEvent::ShotDatum(_)
111 | BinaryEvent::Keepalive(_)
112 | BinaryEvent::Message(_) => {
113 // on_recv callback already printed it
114 }
115 }
116 }
117 }
118}More examples
47fn run() -> Result<(), ironsight::ConnError> {
48 let addr: SocketAddr = DEFAULT_ADDR.parse().unwrap();
49 println!("=== Mode Change Integration Test ===\n");
50
51 // ── Step 1: Connect ──────────────────────────────────────────────────
52 println!("[connect] connecting to {addr}...");
53 let mut conn = BinaryConnection::connect_timeout(&addr, Duration::from_secs(5))?;
54 println!("[connect] OK\n");
55
56 conn.set_on_send(|cmd, dest| {
57 eprintln!(" >> {dest:?} {}", cmd.debug_hex(dest));
58 });
59 conn.set_on_recv(|env| {
60 let hex: String = env.raw.iter().map(|b| format!("{b:02X}")).collect();
61 eprintln!(" << 0x{:02X} {hex} | {:?}", env.type_id, env.message);
62 });
63
64 let mut client = BinaryClient::from_tcp(conn)?;
65 client.set_keepalive_interval(Duration::from_secs(3));
66
67 // Enqueue: handshake → configure(outdoor) → cam → arm
68 client.handshake();
69 client.configure_avr(avr_settings_full(MODE_OUTDOOR));
70 client.configure_cam(cam_config());
71 client.arm();
72
73 let mut step = Step::InitialArm;
74 let mut phase_start = Instant::now();
75 let timeout = Duration::from_secs(60);
76
77 // Track sub-events within InitialArm
78 let mut handshake_done = false;
79 let mut configure_count = 0u8; // need 2: AVR + cam
80
81 println!("[step] InitialArm: handshake → configure(outdoor/9) → cam → arm");
82
83 loop {
84 if phase_start.elapsed() > timeout {
85 return Err(ironsight::ConnError::Protocol(format!(
86 "timeout in step {step:?} after {timeout:?}"
87 )));
88 }
89
90 let event = client.poll()?;
91 let Some(event) = event else {
92 continue;
93 };
94
95 match (&step, &event) {
96 // ── InitialArm phase ──────────────────────────────────────
97 (Step::InitialArm, BinaryEvent::Handshake(h)) => {
98 println!(
99 " [handshake] OK — {} | battery {}%",
100 h.avr.dev_info.text.trim(),
101 h.dsp.status.battery_percent()
102 );
103 handshake_done = true;
104 }
105 (Step::InitialArm, BinaryEvent::Configured) => {
106 configure_count += 1;
107 let label = if configure_count == 1 { "AVR" } else { "cam" };
108 println!(" [configured] {label} OK");
109 }
110 (Step::InitialArm, BinaryEvent::Armed) => {
111 println!(" [armed] OK — device armed in outdoor mode");
112 assert!(handshake_done, "armed before handshake");
113 assert!(configure_count >= 2, "armed before both configures");
114
115 // Transition: wait for keepalive
116 step = Step::WaitKeepalive;
117 phase_start = Instant::now();
118 println!("\n[step] WaitKeepalive: waiting for keepalive...");
119 }
120
121 // ── WaitKeepalive ─────────────────────────────────────────
122 (Step::WaitKeepalive, BinaryEvent::Keepalive(snap)) => {
123 let avr = snap.avr.as_ref().map(|a| format!("tilt={:.1}", a.tilt));
124 let dsp = snap
125 .dsp
126 .as_ref()
127 .map(|d| format!("bat={}%", d.battery_percent()));
128 println!(" [keepalive] OK — {} {}", dsp.unwrap_or_default(), avr.unwrap_or_default());
129
130 // Transition: first mode change outdoor → chipping
131 step = Step::ModeChange1;
132 phase_start = Instant::now();
133 println!("\n[step] ModeChange1: outdoor(9) → chipping(5)");
134 client.configure_avr(avr_settings_mode_only(MODE_CHIPPING));
135 client.arm();
136 }
137
138 // ── ModeChange1 (outdoor → chipping) ─────────────────────
139 (Step::ModeChange1, BinaryEvent::Disarmed) => {
140 println!(" [disarmed] OK");
141 }
142 (Step::ModeChange1, BinaryEvent::Configured) => {
143 println!(" [configured] chipping OK");
144 }
145 (Step::ModeChange1, BinaryEvent::Armed) => {
146 println!(" [armed] OK — chipping mode");
147
148 step = Step::WaitKeepalive2;
149 phase_start = Instant::now();
150 println!("\n[step] WaitKeepalive2: waiting for keepalive...");
151 }
152
153 // ── WaitKeepalive2 ────────────────────────────────────────
154 (Step::WaitKeepalive2, BinaryEvent::Keepalive(snap)) => {
155 let dsp = snap
156 .dsp
157 .as_ref()
158 .map(|d| format!("bat={}%", d.battery_percent()));
159 println!(" [keepalive] OK — {}", dsp.unwrap_or_default());
160
161 // Transition: second mode change chipping → putting
162 step = Step::ModeChange2;
163 phase_start = Instant::now();
164 println!("\n[step] ModeChange2: chipping(5) → putting(3)");
165 client.configure_avr(avr_settings_mode_only(MODE_PUTTING));
166 client.arm();
167 }
168
169 // ── ModeChange2 (chipping → putting) ─────────────────────
170 (Step::ModeChange2, BinaryEvent::Disarmed) => {
171 println!(" [disarmed] OK");
172 }
173 (Step::ModeChange2, BinaryEvent::Configured) => {
174 println!(" [configured] putting OK");
175 }
176 (Step::ModeChange2, BinaryEvent::Armed) => {
177 println!(" [armed] OK — putting mode");
178
179 step = Step::WaitKeepalive3;
180 phase_start = Instant::now();
181 println!("\n[step] WaitKeepalive3: waiting for keepalive...");
182 }
183
184 // ── WaitKeepalive3 ────────────────────────────────────────
185 (Step::WaitKeepalive3, BinaryEvent::Keepalive(snap)) => {
186 let dsp = snap
187 .dsp
188 .as_ref()
189 .map(|d| format!("bat={}%", d.battery_percent()));
190 println!(" [keepalive] OK — {}", dsp.unwrap_or_default());
191
192 step = Step::Done;
193 }
194
195 // ── Done ──────────────────────────────────────────────────
196 (Step::Done, _) => unreachable!(),
197
198 // ── Passthrough (text, messages, etc.) ────────────────────
199 (_, BinaryEvent::Message(_)) => {}
200 (_, BinaryEvent::Keepalive(_)) => {} // extra keepalives
201 (_, other) => {
202 println!(" [unexpected] {other:?} in step {step:?}");
203 }
204 }
205
206 if step == Step::Done {
207 break;
208 }
209 }
210
211 println!("\n=== ALL STEPS PASSED ===");
212 println!(" 1. Connect + handshake + configure(outdoor) + arm ✓");
213 println!(" 2. Keepalive received while armed ✓");
214 println!(" 3. Mode change outdoor → chipping ✓");
215 println!(" 4. Keepalive after mode change ✓");
216 println!(" 5. Mode change chipping → putting ✓");
217 println!(" 6. Keepalive after second mode change ✓");
218
219 Ok(())
220}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}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?
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}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?
33fn run() -> Result<(), ironsight::ConnError> {
34 let addr: SocketAddr = DEFAULT_ADDR.parse().unwrap();
35 println!("Connecting to {addr}...");
36 let mut conn = BinaryConnection::connect_timeout(&addr, Duration::from_secs(5))?;
37 println!("Connected.");
38
39 conn.set_on_send(|cmd, dest| println!(">> {}", cmd.debug_hex(dest)));
40 conn.set_on_recv(|env| println!("<< {env:?}"));
41
42 let mut client = BinaryClient::from_tcp(conn)?;
43
44 // Enqueue the full startup sequence.
45 client.handshake();
46 client.configure_avr(default_avr_settings());
47 client.configure_cam(default_cam_config());
48 client.arm();
49
50 println!("Starting poll loop...");
51 loop {
52 if let Some(event) = client.poll()? {
53 match event {
54 BinaryEvent::Handshake(h) => {
55 println!("\n=== Handshake complete ===");
56 println!(" SSID: {} password: {}", h.pi.ssid, h.pi.password);
57 println!(" DSP: {}", h.dsp.dev_info.text);
58 println!(" AVR: {}", h.avr.dev_info.text);
59 println!(" PI: {}", h.pi.dev_info.text);
60 println!(
61 " Battery: {}%{}",
62 h.dsp.status.battery_percent(),
63 if h.dsp.status.external_power() {
64 " (plugged in)"
65 } else {
66 ""
67 },
68 );
69 }
70 BinaryEvent::Configured => {
71 println!("\n=== Configured ===");
72 }
73 BinaryEvent::Armed => {
74 println!("\n=== ARMED — waiting for shots ===");
75 }
76 BinaryEvent::Trigger => {
77 println!("\n ** BALL TRIGGER **");
78 }
79 BinaryEvent::ShotComplete(data) => {
80 println!("\n === Shot complete ===");
81 if let Some(ref f) = data.flight {
82 println!(
83 " Ball: {:.1} mph Carry: {:.0} yd VLA: {:.1}° HLA: {:.1}°",
84 ms_to_mph(f.launch_speed),
85 m_to_yd(f.carry_distance),
86 f.launch_elevation,
87 f.launch_azimuth,
88 );
89 }
90 if let Some(ref c) = data.club {
91 println!(
92 " Club: {:.1} mph Smash: {:.2} Path: {:.1}° Face: {:.1}°",
93 ms_to_mph(c.pre_club_speed),
94 c.smash_factor,
95 c.strike_direction,
96 c.face_angle,
97 );
98 }
99 if let Some(ref s) = data.spin {
100 println!(
101 " Spin: {} rpm Axis: {:.1}°",
102 s.pm_spin_final, s.spin_axis,
103 );
104 }
105 println!(" === RE-ARMED ===");
106 }
107 BinaryEvent::Disarmed => {
108 println!("\n=== Disarmed ===");
109 }
110 BinaryEvent::ShotDatum(_)
111 | BinaryEvent::Keepalive(_)
112 | BinaryEvent::Message(_) => {
113 // on_recv callback already printed it
114 }
115 }
116 }
117 }
118}More examples
47fn run() -> Result<(), ironsight::ConnError> {
48 let addr: SocketAddr = DEFAULT_ADDR.parse().unwrap();
49 println!("=== Mode Change Integration Test ===\n");
50
51 // ── Step 1: Connect ──────────────────────────────────────────────────
52 println!("[connect] connecting to {addr}...");
53 let mut conn = BinaryConnection::connect_timeout(&addr, Duration::from_secs(5))?;
54 println!("[connect] OK\n");
55
56 conn.set_on_send(|cmd, dest| {
57 eprintln!(" >> {dest:?} {}", cmd.debug_hex(dest));
58 });
59 conn.set_on_recv(|env| {
60 let hex: String = env.raw.iter().map(|b| format!("{b:02X}")).collect();
61 eprintln!(" << 0x{:02X} {hex} | {:?}", env.type_id, env.message);
62 });
63
64 let mut client = BinaryClient::from_tcp(conn)?;
65 client.set_keepalive_interval(Duration::from_secs(3));
66
67 // Enqueue: handshake → configure(outdoor) → cam → arm
68 client.handshake();
69 client.configure_avr(avr_settings_full(MODE_OUTDOOR));
70 client.configure_cam(cam_config());
71 client.arm();
72
73 let mut step = Step::InitialArm;
74 let mut phase_start = Instant::now();
75 let timeout = Duration::from_secs(60);
76
77 // Track sub-events within InitialArm
78 let mut handshake_done = false;
79 let mut configure_count = 0u8; // need 2: AVR + cam
80
81 println!("[step] InitialArm: handshake → configure(outdoor/9) → cam → arm");
82
83 loop {
84 if phase_start.elapsed() > timeout {
85 return Err(ironsight::ConnError::Protocol(format!(
86 "timeout in step {step:?} after {timeout:?}"
87 )));
88 }
89
90 let event = client.poll()?;
91 let Some(event) = event else {
92 continue;
93 };
94
95 match (&step, &event) {
96 // ── InitialArm phase ──────────────────────────────────────
97 (Step::InitialArm, BinaryEvent::Handshake(h)) => {
98 println!(
99 " [handshake] OK — {} | battery {}%",
100 h.avr.dev_info.text.trim(),
101 h.dsp.status.battery_percent()
102 );
103 handshake_done = true;
104 }
105 (Step::InitialArm, BinaryEvent::Configured) => {
106 configure_count += 1;
107 let label = if configure_count == 1 { "AVR" } else { "cam" };
108 println!(" [configured] {label} OK");
109 }
110 (Step::InitialArm, BinaryEvent::Armed) => {
111 println!(" [armed] OK — device armed in outdoor mode");
112 assert!(handshake_done, "armed before handshake");
113 assert!(configure_count >= 2, "armed before both configures");
114
115 // Transition: wait for keepalive
116 step = Step::WaitKeepalive;
117 phase_start = Instant::now();
118 println!("\n[step] WaitKeepalive: waiting for keepalive...");
119 }
120
121 // ── WaitKeepalive ─────────────────────────────────────────
122 (Step::WaitKeepalive, BinaryEvent::Keepalive(snap)) => {
123 let avr = snap.avr.as_ref().map(|a| format!("tilt={:.1}", a.tilt));
124 let dsp = snap
125 .dsp
126 .as_ref()
127 .map(|d| format!("bat={}%", d.battery_percent()));
128 println!(" [keepalive] OK — {} {}", dsp.unwrap_or_default(), avr.unwrap_or_default());
129
130 // Transition: first mode change outdoor → chipping
131 step = Step::ModeChange1;
132 phase_start = Instant::now();
133 println!("\n[step] ModeChange1: outdoor(9) → chipping(5)");
134 client.configure_avr(avr_settings_mode_only(MODE_CHIPPING));
135 client.arm();
136 }
137
138 // ── ModeChange1 (outdoor → chipping) ─────────────────────
139 (Step::ModeChange1, BinaryEvent::Disarmed) => {
140 println!(" [disarmed] OK");
141 }
142 (Step::ModeChange1, BinaryEvent::Configured) => {
143 println!(" [configured] chipping OK");
144 }
145 (Step::ModeChange1, BinaryEvent::Armed) => {
146 println!(" [armed] OK — chipping mode");
147
148 step = Step::WaitKeepalive2;
149 phase_start = Instant::now();
150 println!("\n[step] WaitKeepalive2: waiting for keepalive...");
151 }
152
153 // ── WaitKeepalive2 ────────────────────────────────────────
154 (Step::WaitKeepalive2, BinaryEvent::Keepalive(snap)) => {
155 let dsp = snap
156 .dsp
157 .as_ref()
158 .map(|d| format!("bat={}%", d.battery_percent()));
159 println!(" [keepalive] OK — {}", dsp.unwrap_or_default());
160
161 // Transition: second mode change chipping → putting
162 step = Step::ModeChange2;
163 phase_start = Instant::now();
164 println!("\n[step] ModeChange2: chipping(5) → putting(3)");
165 client.configure_avr(avr_settings_mode_only(MODE_PUTTING));
166 client.arm();
167 }
168
169 // ── ModeChange2 (chipping → putting) ─────────────────────
170 (Step::ModeChange2, BinaryEvent::Disarmed) => {
171 println!(" [disarmed] OK");
172 }
173 (Step::ModeChange2, BinaryEvent::Configured) => {
174 println!(" [configured] putting OK");
175 }
176 (Step::ModeChange2, BinaryEvent::Armed) => {
177 println!(" [armed] OK — putting mode");
178
179 step = Step::WaitKeepalive3;
180 phase_start = Instant::now();
181 println!("\n[step] WaitKeepalive3: waiting for keepalive...");
182 }
183
184 // ── WaitKeepalive3 ────────────────────────────────────────
185 (Step::WaitKeepalive3, BinaryEvent::Keepalive(snap)) => {
186 let dsp = snap
187 .dsp
188 .as_ref()
189 .map(|d| format!("bat={}%", d.battery_percent()));
190 println!(" [keepalive] OK — {}", dsp.unwrap_or_default());
191
192 step = Step::Done;
193 }
194
195 // ── Done ──────────────────────────────────────────────────
196 (Step::Done, _) => unreachable!(),
197
198 // ── Passthrough (text, messages, etc.) ────────────────────
199 (_, BinaryEvent::Message(_)) => {}
200 (_, BinaryEvent::Keepalive(_)) => {} // extra keepalives
201 (_, other) => {
202 println!(" [unexpected] {other:?} in step {step:?}");
203 }
204 }
205
206 if step == Step::Done {
207 break;
208 }
209 }
210
211 println!("\n=== ALL STEPS PASSED ===");
212 println!(" 1. Connect + handshake + configure(outdoor) + arm ✓");
213 println!(" 2. Keepalive received while armed ✓");
214 println!(" 3. Mode change outdoor → chipping ✓");
215 println!(" 4. Keepalive after mode change ✓");
216 println!(" 5. Mode change chipping → putting ✓");
217 println!(" 6. Keepalive after second mode change ✓");
218
219 Ok(())
220}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}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.