Skip to main content

mode_change_test/
mode_change_test.rs

1//! Live device integration test: connect, arm, mode change, keepalive, mode change.
2//!
3//! Usage: cargo run --example mode_change_test
4//!
5//! Requires: connected to Mevo+ WiFi (192.168.2.1:5100).
6
7use std::net::SocketAddr;
8use std::process;
9use std::time::{Duration, Instant};
10
11use ironsight::client::{BinaryClient, BinaryEvent};
12use ironsight::conn::DEFAULT_ADDR;
13use ironsight::protocol::camera::CamConfig;
14use ironsight::protocol::config::{
15    MODE_CHIPPING, MODE_OUTDOOR, MODE_PUTTING, ParamData, ParamValue, RadarCal,
16};
17use ironsight::seq::AvrSettings;
18use ironsight::BinaryConnection;
19
20// ── Test steps ───────────────────────────────────────────────────────────────
21
22#[derive(Debug, Clone, Copy, PartialEq)]
23enum Step {
24    /// Handshake + initial configure + cam configure + arm
25    InitialArm,
26    /// Wait for at least one keepalive while armed
27    WaitKeepalive,
28    /// First mode change: Outdoor → Chipping (disarm → configure → arm)
29    ModeChange1,
30    /// Wait for keepalive after first mode change
31    WaitKeepalive2,
32    /// Second mode change: Chipping → Putting (disarm → configure → arm)
33    ModeChange2,
34    /// Wait for keepalive after second mode change
35    WaitKeepalive3,
36    /// All done
37    Done,
38}
39
40fn main() {
41    if let Err(e) = run() {
42        eprintln!("\n✗ FAILED: {e}");
43        process::exit(1);
44    }
45}
46
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}
221
222// ── Helpers ──────────────────────────────────────────────────────────────────
223
224fn avr_settings_full(mode: u8) -> AvrSettings {
225    AvrSettings {
226        mode,
227        params: vec![
228            ParamValue {
229                param_id: 0x06,
230                value: ParamData::Int24(0), // standard ball
231            },
232            ParamValue {
233                param_id: 0x0F,
234                value: ParamData::Float40(0.8), // 80% track
235            },
236            ParamValue {
237                param_id: 0x26,
238                value: ParamData::Float40(0.0381), // 1.5" tee height
239            },
240        ],
241        radar_cal: Some(RadarCal {
242            range_mm: 2133, // ~7 feet
243            height_mm: 0,
244        }),
245    }
246}
247
248/// Minimal mode change settings: ModeSet only, no params, no RadarCal.
249/// This is the fastest mode change — tested and working in run 7.
250fn avr_settings_mode_only(mode: u8) -> AvrSettings {
251    AvrSettings {
252        mode,
253        params: vec![],
254        radar_cal: None,
255    }
256}
257
258fn cam_config() -> CamConfig {
259    CamConfig {
260        dynamic_config: true,
261        resolution_width: 1024,
262        resolution_height: 768,
263        rotation: 0,
264        ev: 0,
265        quality: 80,
266        framerate: 20,
267        streaming_framerate: 1,
268        ringbuffer_pretime_ms: 1000,
269        ringbuffer_posttime_ms: 4000,
270        raw_camera_mode: 0,
271        fusion_camera_mode: false,
272        raw_shutter_speed_max: 0.0,
273        raw_ev_roi_x: 0,
274        raw_ev_roi_y: 0,
275        raw_ev_roi_width: 0,
276        raw_ev_roi_height: 0,
277        raw_x_offset: 0,
278        raw_bin44: false,
279        raw_live_preview_write_interval_ms: 0,
280        raw_y_offset: 0,
281        buffer_sub_sampling_pre_trigger_div: 1,
282        buffer_sub_sampling_post_trigger_div: 1,
283        buffer_sub_sampling_switch_time_offset: 0.0,
284        buffer_sub_sampling_total_buffer_size: 0,
285        buffer_sub_sampling_pre_trigger_buffer_size: 0,
286    }
287}