pokeys_lib/
pulse_engine.rs

1//! Pulse Engine v2 support for motion control
2
3use crate::device::PoKeysDevice;
4use crate::error::{PoKeysError, Result};
5use crate::types::{PulseEngineAxisState, PulseEngineState};
6use serde::{Deserialize, Serialize};
7
8/// Motor driver step setting constants
9pub mod step_setting {
10    pub const FULL_STEP: u8 = 0; // 1/1
11    pub const HALF_NON_CIRCULAR: u8 = 1; // 1/2 non-circular
12    pub const HALF_STEP: u8 = 2; // 1/2
13    pub const QUARTER_STEP: u8 = 3; // 1/4
14    pub const EIGHTH_STEP: u8 = 4; // 1/8
15    pub const SIXTEENTH_STEP: u8 = 5; // 1/16
16}
17
18/// Step resolution options for stepper motors
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum StepResolution {
21    FullStep,
22    HalfStep,
23    QuarterStep,
24    EighthStep,
25    SixteenthStep,
26}
27
28impl StepResolution {
29    pub fn to_u8(self) -> u8 {
30        match self {
31            StepResolution::FullStep => step_setting::FULL_STEP,
32            StepResolution::HalfStep => step_setting::HALF_STEP,
33            StepResolution::QuarterStep => step_setting::QUARTER_STEP,
34            StepResolution::EighthStep => step_setting::EIGHTH_STEP,
35            StepResolution::SixteenthStep => step_setting::SIXTEENTH_STEP,
36        }
37    }
38
39    pub fn multiplier(self) -> f32 {
40        match self {
41            StepResolution::FullStep => 1.0,
42            StepResolution::HalfStep => 0.5,
43            StepResolution::QuarterStep => 0.25,
44            StepResolution::EighthStep => 0.125,
45            StepResolution::SixteenthStep => 0.0625,
46        }
47    }
48    pub const THIRTY_SECOND_STEP: u8 = 6; // 1/32
49    pub const STEP_128: u8 = 7; // 1/128
50    pub const STEP_256: u8 = 8; // 1/256
51}
52
53/// Pulse engine power states (bit-mapped)
54pub struct PulseEnginePowerState;
55
56impl PulseEnginePowerState {
57    // States with enabled power (bit-mapped)
58    pub const PE_STOPPED: u8 = 1 << 0;
59    pub const PE_STOP_LIMIT: u8 = 1 << 1;
60    pub const PE_STOP_EMERGENCY: u8 = 1 << 2;
61
62    // States with enabled charge pump (bit-mapped)
63    pub const PE_STOPPED_CHARGE_PUMP: u8 = 1 << 4;
64    pub const PE_STOP_LIMIT_CHARGE_PUMP: u8 = 1 << 5;
65    pub const PE_STOP_EMERGENCY_CHARGE_PUMP: u8 = 1 << 6;
66
67    // Common combinations
68    pub const ALL_POWER_ENABLED: u8 =
69        Self::PE_STOPPED | Self::PE_STOP_LIMIT | Self::PE_STOP_EMERGENCY;
70    pub const ALL_CHARGE_PUMP_ENABLED: u8 = Self::PE_STOPPED_CHARGE_PUMP
71        | Self::PE_STOP_LIMIT_CHARGE_PUMP
72        | Self::PE_STOP_EMERGENCY_CHARGE_PUMP;
73}
74
75/// Pulse engine setup configuration
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct PulseEngineConfig {
78    pub enabled_axes: u8,
79    pub charge_pump_enabled: u8,
80    pub generator_type: u8,
81    pub buffer_size: u8,
82    pub emergency_switch_polarity: u8,
83    pub power_states: u8,
84}
85
86impl PulseEngineConfig {
87    /// Create builder for 3-channel internal generator
88    pub fn three_channel_internal(axes: u8, swap_step_dir: bool) -> PulseEngineConfigBuilder {
89        PulseEngineConfigBuilder {
90            enabled_axes: axes,
91            charge_pump_enabled: 0,
92            generator_type: if swap_step_dir { 0x41 } else { 0x01 },
93            buffer_size: 0,
94            emergency_switch_polarity: 1,
95            power_states: PulseEnginePowerState::ALL_POWER_ENABLED,
96        }
97    }
98
99    /// Create configuration for 8-channel external generator
100    pub fn eight_channel_external(axes: u8) -> Self {
101        Self {
102            enabled_axes: axes,
103            charge_pump_enabled: 0,
104            generator_type: 0, // 8ch external
105            buffer_size: 0,    // default
106            emergency_switch_polarity: 1,
107            power_states: PulseEnginePowerState::ALL_POWER_ENABLED,
108        }
109    }
110}
111
112pub struct PulseEngineConfigBuilder {
113    enabled_axes: u8,
114    charge_pump_enabled: u8,
115    generator_type: u8,
116    buffer_size: u8,
117    emergency_switch_polarity: u8,
118    power_states: u8,
119}
120
121impl PulseEngineConfigBuilder {
122    pub fn charge_pump_enabled(mut self, enabled: u8) -> Self {
123        self.charge_pump_enabled = enabled;
124        self
125    }
126
127    pub fn buffer_size(mut self, size: u8) -> Self {
128        self.buffer_size = size;
129        self
130    }
131
132    pub fn emergency_switch_polarity(mut self, polarity: u8) -> Self {
133        self.emergency_switch_polarity = polarity;
134        self
135    }
136
137    pub fn power_states(mut self, states: u8) -> Self {
138        self.power_states = states;
139        self
140    }
141
142    pub fn build(self) -> PulseEngineConfig {
143        PulseEngineConfig {
144            enabled_axes: self.enabled_axes,
145            charge_pump_enabled: self.charge_pump_enabled,
146            generator_type: self.generator_type,
147            buffer_size: self.buffer_size,
148            emergency_switch_polarity: self.emergency_switch_polarity,
149            power_states: self.power_states,
150        }
151    }
152}
153
154/// Pulse Engine v2 information
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct PulseEngineV2Info {
157    pub nr_of_axes: u8,
158    pub max_pulse_frequency: u8,
159    pub buffer_depth: u8,
160    pub slot_timing: u8,
161}
162
163/// Pulse Engine v2 main structure
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct PulseEngineV2 {
166    pub info: PulseEngineV2Info,
167    pub axes_state: [u8; 8],
168    pub axes_config: [u8; 8],
169    pub axes_switch_config: [u8; 8],
170    pub current_position: [i32; 8],
171    pub position_setup: [i32; 8],
172    pub reference_position_speed: [i32; 8],
173    pub invert_axis_enable: [i8; 8],
174    pub soft_limit_maximum: [i32; 8],
175    pub soft_limit_minimum: [i32; 8],
176    pub homing_speed: [u8; 8],
177    pub homing_return_speed: [u8; 8],
178    pub home_offsets: [i32; 8],
179    pub homing_algorithm: [u8; 8],
180    pub filter_limit_m_switch: [u8; 8],
181    pub filter_limit_p_switch: [u8; 8],
182    pub filter_home_switch: [u8; 8],
183    pub probe_position: [i32; 8],
184    pub probe_max_position: [i32; 8],
185    pub max_speed: [f32; 8],
186    pub max_acceleration: [f32; 8],
187    pub max_deceleration: [f32; 8],
188    pub mpg_jog_multiplier: [i32; 8],
189    pub mpg_jog_encoder: [u8; 8],
190    pub pin_home_switch: [u8; 8],
191    pub pin_limit_m_switch: [u8; 8],
192    pub pin_limit_p_switch: [u8; 8],
193    pub axis_enable_output_pins: [u8; 8],
194    pub home_back_off_distance: [u32; 8],
195    pub mpg_jog_divider: [u16; 8],
196    pub filter_probe_input: u8,
197    pub axis_signal_options: [u8; 8],
198    pub reference_velocity_pv: [f32; 8],
199    pub pulse_engine_enabled: u8,
200    pub pulse_generator_type: u8,
201    pub charge_pump_enabled: u8,
202    pub emergency_switch_polarity: u8,
203    pub pulse_engine_activated: u8,
204    pub limit_status_p: u8,
205    pub limit_status_n: u8,
206    pub home_status: u8,
207    pub error_input_status: u8,
208    pub misc_input_status: u8,
209    pub limit_override: u8,
210    pub limit_override_setup: u8,
211    pub pulse_engine_state: u8,
212    pub axis_enabled_mask: u8,
213    pub emergency_input_pin: u8,
214    pub sync_fast_outputs_axis_id: u8,
215    pub sync_fast_outputs_mapping: [u8; 8],
216    pub param1: u8,
217    pub param2: u8,
218    pub param3: u8,
219    pub axis_enabled_states_mask: u8,
220    pub pulse_engine_state_setup: u8,
221    pub soft_limit_status: u8,
222    pub external_relay_outputs: u8,
223    pub external_oc_outputs: u8,
224    pub pulse_engine_buffer_size: u8,
225    pub motion_buffer_entries_accepted: u8,
226    pub new_motion_buffer_entries: u8,
227    pub homing_start_mask_setup: u8,
228    pub probe_start_mask_setup: u8,
229    pub probe_input: u8,
230    pub probe_input_polarity: u8,
231    pub probe_status: u8,
232    pub motion_buffer: Vec<u8>,
233    pub probe_speed: f32,
234    pub debug_values: [u32; 16],
235    pub backlash_width: [u16; 8],
236    pub backlash_register: [i16; 8],
237    pub backlash_acceleration: [u8; 8],
238    pub backlash_compensation_enabled: u8,
239    pub backlash_compensation_max_speed: u8,
240    pub trigger_preparing: u8,
241    pub trigger_prepared: u8,
242    pub trigger_pending: u8,
243    pub trigger_active: u8,
244    pub spindle_speed_estimate: i32,
245    pub spindle_position_error: i32,
246    pub motor_step_setting: [u8; 4],
247    pub motor_current_setting: [u8; 4],
248    pub spindle_rpm: u32,
249    pub spindle_index_counter: u32,
250    pub dedicated_limit_n_inputs: u8,
251    pub dedicated_limit_p_inputs: u8,
252    pub dedicated_home_inputs: u8,
253    pub trigger_ignored_axis_mask: u8,
254    pub encoder_index_count: u32,
255    pub encoder_ticks_per_rotation: u32,
256    pub encoder_velocity: u32,
257    pub internal_driver_step_config: [u8; 4],
258    pub internal_driver_current_config: [u8; 4],
259}
260
261impl PulseEngineV2 {
262    pub fn new() -> Self {
263        Self {
264            info: PulseEngineV2Info {
265                nr_of_axes: 0,
266                max_pulse_frequency: 0,
267                buffer_depth: 0,
268                slot_timing: 0,
269            },
270            axes_state: [0; 8],
271            axes_config: [0; 8],
272            axes_switch_config: [0; 8],
273            current_position: [0; 8],
274            position_setup: [0; 8],
275            reference_position_speed: [0; 8],
276            invert_axis_enable: [0; 8],
277            soft_limit_maximum: [0; 8],
278            soft_limit_minimum: [0; 8],
279            homing_speed: [0; 8],
280            homing_return_speed: [0; 8],
281            home_offsets: [0; 8],
282            homing_algorithm: [0; 8],
283            filter_limit_m_switch: [0; 8],
284            filter_limit_p_switch: [0; 8],
285            filter_home_switch: [0; 8],
286            probe_position: [0; 8],
287            probe_max_position: [0; 8],
288            max_speed: [0.0; 8],
289            max_acceleration: [0.0; 8],
290            max_deceleration: [0.0; 8],
291            mpg_jog_multiplier: [0; 8],
292            mpg_jog_encoder: [0; 8],
293            pin_home_switch: [0; 8],
294            pin_limit_m_switch: [0; 8],
295            pin_limit_p_switch: [0; 8],
296            axis_enable_output_pins: [0; 8],
297            home_back_off_distance: [0; 8],
298            mpg_jog_divider: [0; 8],
299            filter_probe_input: 0,
300            axis_signal_options: [0; 8],
301            reference_velocity_pv: [0.0; 8],
302            pulse_engine_enabled: 0,
303            pulse_generator_type: 0,
304            charge_pump_enabled: 0,
305            emergency_switch_polarity: 0,
306            pulse_engine_activated: 0,
307            limit_status_p: 0,
308            limit_status_n: 0,
309            home_status: 0,
310            error_input_status: 0,
311            misc_input_status: 0,
312            limit_override: 0,
313            limit_override_setup: 0,
314            pulse_engine_state: 0,
315            axis_enabled_mask: 0,
316            emergency_input_pin: 0,
317            sync_fast_outputs_axis_id: 0,
318            sync_fast_outputs_mapping: [0; 8],
319            param1: 0,
320            param2: 0,
321            param3: 0,
322            axis_enabled_states_mask: 0,
323            pulse_engine_state_setup: 0,
324            soft_limit_status: 0,
325            external_relay_outputs: 0,
326            external_oc_outputs: 0,
327            pulse_engine_buffer_size: 0,
328            motion_buffer_entries_accepted: 0,
329            new_motion_buffer_entries: 0,
330            homing_start_mask_setup: 0,
331            probe_start_mask_setup: 0,
332            probe_input: 0,
333            probe_input_polarity: 0,
334            probe_status: 0,
335            motion_buffer: vec![0; 448],
336            probe_speed: 0.0,
337            debug_values: [0; 16],
338            backlash_width: [0; 8],
339            backlash_register: [0; 8],
340            backlash_acceleration: [0; 8],
341            backlash_compensation_enabled: 0,
342            backlash_compensation_max_speed: 0,
343            trigger_preparing: 0,
344            trigger_prepared: 0,
345            trigger_pending: 0,
346            trigger_active: 0,
347            spindle_speed_estimate: 0,
348            spindle_position_error: 0,
349            motor_step_setting: [0; 4],
350            motor_current_setting: [0; 4],
351            spindle_rpm: 0,
352            spindle_index_counter: 0,
353            dedicated_limit_n_inputs: 0,
354            dedicated_limit_p_inputs: 0,
355            dedicated_home_inputs: 0,
356            trigger_ignored_axis_mask: 0,
357            encoder_index_count: 0,
358            encoder_ticks_per_rotation: 0,
359            encoder_velocity: 0,
360            internal_driver_step_config: [0; 4],
361            internal_driver_current_config: [0; 4],
362        }
363    }
364
365    pub fn is_enabled(&self) -> bool {
366        self.pulse_engine_enabled != 0
367    }
368
369    pub fn is_activated(&self) -> bool {
370        self.pulse_engine_activated != 0
371    }
372
373    pub fn get_state(&self) -> PulseEngineState {
374        match self.pulse_engine_state {
375            0 => PulseEngineState::Stopped,
376            1 => PulseEngineState::Internal,
377            2 => PulseEngineState::Buffer,
378            3 => PulseEngineState::Running,
379            10 => PulseEngineState::Jogging,
380            11 => PulseEngineState::Stopping,
381            20 => PulseEngineState::Home,
382            21 => PulseEngineState::Homing,
383            30 => PulseEngineState::ProbeComplete,
384            31 => PulseEngineState::Probe,
385            32 => PulseEngineState::ProbeError,
386            40 => PulseEngineState::HybridProbeStopping,
387            41 => PulseEngineState::HybridProbeComplete,
388            100 => PulseEngineState::StopLimit,
389            101 => PulseEngineState::StopEmergency,
390            _ => PulseEngineState::Stopped,
391        }
392    }
393
394    /// Get pulse generator type (bits 0-3)
395    pub fn get_generator_type(&self) -> u8 {
396        self.pulse_generator_type & 0x0F
397    }
398
399    /// Get pulse generator type description
400    pub fn get_generator_type_description(&self) -> &'static str {
401        match self.get_generator_type() {
402            0 => "8ch external",
403            1 => "3ch internal",
404            2 => "8ch smart external",
405            _ => "unknown",
406        }
407    }
408
409    /// Check if step/dir signals are swapped (bit 6)
410    pub fn is_step_dir_swapped(&self) -> bool {
411        self.pulse_generator_type & 0x40 != 0
412    }
413
414    /// Check if external extended IO features are enabled (bit 7)
415    pub fn is_extended_io_enabled(&self) -> bool {
416        self.pulse_generator_type & 0x80 != 0
417    }
418
419    /// Get axis state for a specific axis
420    pub fn get_axis_state(&self, axis: usize) -> PulseEngineAxisState {
421        if axis >= 8 {
422            return PulseEngineAxisState::Stopped;
423        }
424
425        match self.axes_state[axis] {
426            0 => PulseEngineAxisState::Stopped,
427            1 => PulseEngineAxisState::Ready,
428            2 => PulseEngineAxisState::Running,
429            3 => PulseEngineAxisState::Accelerating,
430            4 => PulseEngineAxisState::Decelerating,
431            8 => PulseEngineAxisState::HomingResetting,
432            9 => PulseEngineAxisState::HomingBackingOff,
433            10 => PulseEngineAxisState::Home,
434            11 => PulseEngineAxisState::HomingStart,
435            12 => PulseEngineAxisState::HomingSearch,
436            13 => PulseEngineAxisState::HomingBack,
437            14 => PulseEngineAxisState::Probed,
438            15 => PulseEngineAxisState::ProbeStart,
439            16 => PulseEngineAxisState::ProbeSearch,
440            20 => PulseEngineAxisState::Error,
441            30 => PulseEngineAxisState::Limit,
442            _ => PulseEngineAxisState::Stopped,
443        }
444    }
445
446    /// Check if axis has positive limit switch triggered
447    pub fn is_axis_limit_positive(&self, axis: usize) -> bool {
448        if axis >= 8 {
449            return false;
450        }
451        self.limit_status_p & (1 << axis) != 0
452    }
453
454    /// Check if axis has negative limit switch triggered
455    pub fn is_axis_limit_negative(&self, axis: usize) -> bool {
456        if axis >= 8 {
457            return false;
458        }
459        self.limit_status_n & (1 << axis) != 0
460    }
461
462    /// Check if axis is at home position
463    pub fn is_axis_home(&self, axis: usize) -> bool {
464        if axis >= 8 {
465            return false;
466        }
467        self.home_status & (1 << axis) != 0
468    }
469
470    /// Check if axis has soft limit triggered
471    pub fn is_axis_soft_limit(&self, axis: usize) -> bool {
472        if axis >= 8 {
473            return false;
474        }
475        self.soft_limit_status & (1 << axis) != 0
476    }
477
478    pub fn is_axis_enabled(&self, axis: usize) -> bool {
479        if axis >= 8 {
480            return false;
481        }
482        (self.axis_enabled_mask & (1 << axis)) != 0
483    }
484
485    pub fn is_axis_homed(&self, axis: usize) -> bool {
486        if axis >= 8 {
487            return false;
488        }
489        (self.home_status & (1 << axis)) != 0
490    }
491
492    pub fn is_limit_triggered(&self, axis: usize, positive: bool) -> bool {
493        if axis >= 8 {
494            return false;
495        }
496
497        if positive {
498            (self.limit_status_p & (1 << axis)) != 0
499        } else {
500            (self.limit_status_n & (1 << axis)) != 0
501        }
502    }
503}
504
505impl Default for PulseEngineV2 {
506    fn default() -> Self {
507        Self::new()
508    }
509}
510
511impl PoKeysDevice {
512    /// Enable pulse engine
513    pub fn enable_pulse_engine(&mut self, enable: bool) -> Result<()> {
514        self.pulse_engine_v2.pulse_engine_enabled = if enable { 1 } else { 0 };
515        self.send_request(0x80, if enable { 1 } else { 0 }, 0, 0, 0)?;
516        Ok(())
517    }
518
519    /// Activate pulse engine
520    pub fn activate_pulse_engine(&mut self, activate: bool) -> Result<()> {
521        self.pulse_engine_v2.pulse_engine_activated = if activate { 1 } else { 0 };
522        self.send_request(0x81, if activate { 1 } else { 0 }, 0, 0, 0)?;
523        Ok(())
524    }
525
526    /// Setup pulse engine (0x85/0x01)
527    /// Setup pulse engine (0x85/0x01) with axis enable mask
528    pub fn setup_pulse_engine_with_axes(
529        &mut self,
530        config: &PulseEngineConfig,
531        axis_enable_mask: u8,
532    ) -> Result<()> {
533        let mut request = vec![0u8; 56]; // Only data payload (protocol bytes 9-64)
534
535        // Build request according to official PoKeysLib implementation
536        request[0] = config.enabled_axes; // Protocol byte 9: Number of enabled axes
537        request[1] = config.charge_pump_enabled; // Protocol byte 10: Safety charge pump
538        request[2] = config.generator_type & 0x7F; // Protocol byte 11: Generator configuration
539        request[3] = config.buffer_size; // Protocol byte 12: Motion buffer size
540        request[4] = config.emergency_switch_polarity; // Protocol byte 13: Emergency switch polarity
541        request[5] = axis_enable_mask; // Protocol byte 14: AxisEnabledStatesMask (like official PoKeysLib)
542
543        // Protocol bytes 15-63 are reserved (already initialized to 0)
544
545        self.send_request_with_data(0x85, 0x01, 0, 0, 0, &request)?;
546        Ok(())
547    }
548
549    pub fn get_pulse_engine_status(&mut self) -> Result<()> {
550        let response = self.send_request(0x85, 0x00, 0, 0, 0)?;
551
552        if response.len() >= 64 {
553            // Parse response according to specification
554            self.pulse_engine_v2.soft_limit_status = response[3];
555            self.pulse_engine_v2.axis_enabled_states_mask = response[4];
556            self.pulse_engine_v2.axis_enabled_mask = response[4]; // Update the field used by is_axis_enabled
557            self.pulse_engine_v2.limit_override = response[5];
558            // Skip request ID (6) and checksum (7)
559            self.pulse_engine_v2.info.nr_of_axes = response[8];
560            self.pulse_engine_v2.pulse_engine_activated = response[9];
561            self.pulse_engine_v2.pulse_engine_state = response[10];
562            self.pulse_engine_v2.charge_pump_enabled = response[11];
563            self.pulse_engine_v2.limit_status_p = response[12];
564            self.pulse_engine_v2.limit_status_n = response[13];
565            self.pulse_engine_v2.home_status = response[14];
566            self.pulse_engine_v2.pulse_generator_type = response[15];
567
568            // Parse axes status (bytes 16-23)
569            for i in 0..8 {
570                if 16 + i < response.len() {
571                    self.pulse_engine_v2.axes_state[i] = response[16 + i];
572                }
573            }
574
575            // Parse axes positions (bytes 24-55, 8x 32-bit LSB first)
576            for i in 0..8 {
577                let base = 24 + i * 4;
578                if base + 3 < response.len() {
579                    self.pulse_engine_v2.current_position[i] = i32::from_le_bytes([
580                        response[base],
581                        response[base + 1],
582                        response[base + 2],
583                        response[base + 3],
584                    ]);
585                }
586            }
587
588            // Parse remaining fields
589            if response.len() >= 63 {
590                self.pulse_engine_v2.info.max_pulse_frequency = response[57];
591                self.pulse_engine_v2.info.buffer_depth = response[58];
592                self.pulse_engine_v2.info.slot_timing = response[59];
593                self.pulse_engine_v2.emergency_switch_polarity = response[60];
594                self.pulse_engine_v2.error_input_status = response[61];
595                self.pulse_engine_v2.misc_input_status = response[62];
596            }
597        }
598
599        Ok(())
600    }
601
602    /// Set axis position (0x85/0x03)
603    pub fn set_axis_position(&mut self, axis: usize, position: i32) -> Result<()> {
604        if axis >= 8 {
605            return Err(PoKeysError::Parameter("Axis index must be 0-7".to_string()));
606        }
607
608        let mut request = vec![0u8; 56]; // Only data payload (protocol bytes 9-64)
609
610        // Protocol bytes 9-40: Axis positions (32-bit LSB first)
611        // Each axis position is 4 bytes, so axis N starts at byte N*4
612        let pos_bytes = position.to_le_bytes();
613        let start_idx = axis * 4;
614        request[start_idx..start_idx + 4].copy_from_slice(&pos_bytes);
615
616        // Protocol byte 4: Axis position setup selection (bit-mapped)
617        let axis_mask = 1u8 << axis;
618
619        // Protocol bytes 41-63 are reserved (already initialized to 0)
620
621        self.send_request_with_data(0x85, 0x03, axis_mask, 0, 0, &request)?;
622        Ok(())
623    }
624
625    /// Set pulse engine state (0x85/0x02)
626    pub fn set_pulse_engine_state(
627        &mut self,
628        state: u8,
629        limit_override: u8,
630        output_enable_mask: u8,
631    ) -> Result<()> {
632        let request = vec![0u8; 55]; // Reserved bytes 9-63
633        self.send_request_with_data(
634            0x85,
635            0x02,
636            state,
637            limit_override,
638            output_enable_mask,
639            &request,
640        )?;
641        Ok(())
642    }
643
644    /// Reboot pulse engine (0x85/0x05)
645    pub fn reboot_pulse_engine(&mut self) -> Result<()> {
646        let request = vec![0u8; 55]; // Reserved bytes 9-63
647        self.send_request_with_data(0x85, 0x05, 0, 0, 0, &request)?;
648        Ok(())
649    }
650
651    /// Set axis configuration (0x85/0x11)
652    pub fn set_axis_configuration(&mut self, axis: usize) -> Result<()> {
653        if axis >= 8 {
654            return Err(PoKeysError::Parameter("Axis index must be 0-7".to_string()));
655        }
656
657        let mut request = vec![0u8; 56]; // Data payload (protocol bytes 9-64)
658
659        // Byte 9: Axis options - Enable axis with internal planner, soft limits, and masked enable
660        let mut axis_options = 0x01; // aoENABLED
661        axis_options |= 1 << 2; // aoINTERNAL_PLANNER - enables acceleration/deceleration control
662        axis_options |= 1 << 3; // aoPOSITION_MODE - internal position control mode
663        axis_options |= 1 << 6; // aoENABLED_MASKED - required for output enable mask control
664        if self.pulse_engine_v2.soft_limit_minimum[axis] != 0
665            || self.pulse_engine_v2.soft_limit_maximum[axis] != 0
666        {
667            axis_options |= 1 << 5; // aoSOFT_LIMIT_ENABLED
668        }
669        request[0] = axis_options;
670
671        // Byte 10: Axis switch options
672        request[1] = 0x00; // No switches
673
674        // Bytes 11-13: Switch pins (0 for external)
675        request[2] = 0; // Home switch pin
676        request[3] = 0; // Limit- switch pin  
677        request[4] = 0; // Limit+ switch pin
678
679        // Bytes 14-15: Homing speeds
680        request[5] = 50; // Homing speed (50% of max)
681        request[6] = 10; // Homing return speed (10% of homing)
682
683        // Byte 16: MPG jog encoder ID
684        request[7] = 0;
685
686        // Bytes 17-20: Maximum speed (32-bit float in timeslot units)
687        // Convert from pulses/second to timeslot units for 25kHz internal pulse frequency
688        // timeslot_units = pulses_per_second / 25000
689        let speed_timeslots = self.pulse_engine_v2.max_speed[axis] / 25000.0;
690        let speed_bytes = speed_timeslots.to_le_bytes();
691        request[8..12].copy_from_slice(&speed_bytes);
692
693        // Bytes 21-24: Maximum acceleration (32-bit float in timeslot units)
694        let accel_timeslots = self.pulse_engine_v2.max_acceleration[axis] / 25000.0;
695        let accel_bytes = accel_timeslots.to_le_bytes();
696        request[12..16].copy_from_slice(&accel_bytes);
697
698        // Bytes 25-28: Maximum deceleration (32-bit float in timeslot units)
699        let decel_timeslots = self.pulse_engine_v2.max_deceleration[axis] / 25000.0;
700        let decel_bytes = decel_timeslots.to_le_bytes();
701        request[16..20].copy_from_slice(&decel_bytes);
702
703        // Bytes 29-32: Soft-limit minimum position
704        let min_bytes = self.pulse_engine_v2.soft_limit_minimum[axis].to_le_bytes();
705        request[20..24].copy_from_slice(&min_bytes);
706
707        // Bytes 33-36: Soft-limit maximum position
708        let max_bytes = self.pulse_engine_v2.soft_limit_maximum[axis].to_le_bytes();
709        request[24..28].copy_from_slice(&max_bytes);
710
711        // Bytes 37-38: MPG jog multiplier
712        request[28] = 1;
713        request[29] = 0;
714
715        // Byte 39: Axis enable output pin (0 for external)
716        request[30] = 0;
717
718        // Byte 40: Invert axis enable signal
719        request[31] = 0;
720
721        // Bytes 41-43: Filter settings
722        request[32] = 0; // Limit- filter
723        request[33] = 0; // Limit+ filter
724        request[34] = 0; // Home filter
725
726        // Byte 44: Home algorithm
727        request[35] = 0x83; // Default algorithm
728
729        // Bytes 46-49: Home back-off distance
730        let backoff = 0i32;
731        let backoff_bytes = backoff.to_le_bytes();
732        request[37..41].copy_from_slice(&backoff_bytes);
733
734        // Bytes 50-51: MPG encoder divider
735        request[41] = 1;
736        request[42] = 0;
737
738        // Byte 52: Additional misc options
739        request[43] = 0x00; // No inversion
740
741        // Byte 53: Probe filter
742        request[44] = 0;
743
744        // Bytes 54-63: Reserved (already 0)
745
746        // Send the complete configuration using send_request_with_data
747        self.send_request_with_data(0x85, 0x11, axis as u8, 0, 0, &request)?;
748        Ok(())
749    }
750
751    /// Get axis configuration (0x85/0x10)
752    pub fn get_axis_configuration(&mut self, axis: usize) -> Result<()> {
753        if axis >= 8 {
754            return Err(PoKeysError::Parameter("Axis index must be 0-7".to_string()));
755        }
756
757        let response = self.send_request(0x85, 0x10, axis as u8, 0, 0)?;
758
759        if response.len() >= 40 {
760            // Debug: Read back the values we just set
761            let speed_bytes = [response[16], response[17], response[18], response[19]];
762            let accel_bytes = [response[20], response[21], response[22], response[23]];
763            let decel_bytes = [response[24], response[25], response[26], response[27]];
764
765            let readback_speed = f32::from_le_bytes(speed_bytes);
766            let readback_accel = f32::from_le_bytes(accel_bytes);
767            let readback_decel = f32::from_le_bytes(decel_bytes);
768
769            println!(
770                "DEBUG 0x85/0x10 READBACK: speed={:.6}, accel={:.6}, decel={:.6}",
771                readback_speed, readback_accel, readback_decel
772            );
773            println!(
774                "DEBUG ORIGINAL VALUES: speed={:.6}, accel={:.6}, decel={:.6}",
775                self.pulse_engine_v2.max_speed[axis],
776                self.pulse_engine_v2.max_acceleration[axis],
777                self.pulse_engine_v2.max_deceleration[axis]
778            );
779        }
780
781        // Parse response according to specification
782        if response.len() >= 64 {
783            // Byte 9: Axis options
784            self.pulse_engine_v2.axes_config[axis] = response[8];
785
786            // Byte 10: Axis switch options
787            self.pulse_engine_v2.axes_switch_config[axis] = response[9];
788
789            // Byte 14: Homing speed
790            self.pulse_engine_v2.homing_speed[axis] = response[13];
791
792            // Byte 15: Homing return speed
793            self.pulse_engine_v2.homing_return_speed[axis] = response[14];
794
795            // Byte 16: MPG jog encoder ID
796            self.pulse_engine_v2.mpg_jog_encoder[axis] = response[15];
797
798            // Bytes 17-20: Maximum speed (32-bit float)
799            let speed_bytes = [response[16], response[17], response[18], response[19]];
800            self.pulse_engine_v2.max_speed[axis] = f32::from_le_bytes(speed_bytes);
801
802            // Bytes 21-24: Maximum acceleration (32-bit float)
803            let accel_bytes = [response[20], response[21], response[22], response[23]];
804            self.pulse_engine_v2.max_acceleration[axis] = f32::from_le_bytes(accel_bytes);
805
806            // Bytes 25-28: Maximum deceleration (32-bit float)
807            let decel_bytes = [response[24], response[25], response[26], response[27]];
808            self.pulse_engine_v2.max_deceleration[axis] = f32::from_le_bytes(decel_bytes);
809
810            // Bytes 29-32: Soft-limit minimum position (32-bit int)
811            let min_pos_bytes = [response[28], response[29], response[30], response[31]];
812            self.pulse_engine_v2.soft_limit_minimum[axis] = i32::from_le_bytes(min_pos_bytes);
813
814            // Bytes 33-36: Soft-limit maximum position (32-bit int)
815            let max_pos_bytes = [response[32], response[33], response[34], response[35]];
816            self.pulse_engine_v2.soft_limit_maximum[axis] = i32::from_le_bytes(max_pos_bytes);
817        }
818
819        Ok(())
820    }
821    pub fn get_axis_position(&mut self, axis: usize) -> Result<i32> {
822        if axis >= 8 {
823            return Err(PoKeysError::Parameter("Invalid axis number".to_string()));
824        }
825
826        // Read pulse engine status
827        self.read_pulse_engine_status()?;
828
829        Ok(self.pulse_engine_v2.current_position[axis])
830    }
831
832    /// Move axis to position using 0x85/0x20 command in position control mode
833    pub fn move_axis_to_position(&mut self, axis: usize, position: i32, _speed: f32) -> Result<()> {
834        if axis >= 8 {
835            return Err(PoKeysError::Parameter("Invalid axis number".to_string()));
836        }
837
838        // Get current positions first
839        self.get_pulse_engine_status()?;
840
841        // Set the reference position for the axis
842        self.pulse_engine_v2.reference_position_speed[axis] = position;
843
844        // Execute the move using 0x85/0x20 command
845        // Now that axis is in position control mode, this should move to exact position
846        let mut request = vec![0u8; 56]; // Data payload bytes 9-64
847
848        // Send target position for moving axis, current positions for others
849        for i in 0..8 {
850            let pos = if i == axis {
851                position // Target position for moving axis
852            } else {
853                self.pulse_engine_v2.current_position[i] // Current position for other axes
854            };
855            let pos_bytes = pos.to_le_bytes();
856            request[i * 4..(i + 1) * 4].copy_from_slice(&pos_bytes);
857        }
858
859        self.send_request_with_data(0x85, 0x20, 0, 0, 0, &request)?;
860        Ok(())
861    }
862
863    /// Start axis homing
864    pub fn start_axis_homing(&mut self, axis_mask: u8) -> Result<()> {
865        self.pulse_engine_v2.homing_start_mask_setup = axis_mask;
866        self.send_request(0x85, axis_mask, 0, 0, 0)?;
867        Ok(())
868    }
869
870    /// Configure axis homing
871    pub fn configure_axis_homing(
872        &mut self,
873        axis: usize,
874        home_pin: u8,
875        homing_speed: u8,
876        home_offset: i32,
877    ) -> Result<()> {
878        if axis >= 8 {
879            return Err(PoKeysError::Parameter("Invalid axis number".to_string()));
880        }
881
882        self.pulse_engine_v2.pin_home_switch[axis] = home_pin;
883        self.pulse_engine_v2.homing_speed[axis] = homing_speed;
884        self.pulse_engine_v2.home_offsets[axis] = home_offset;
885
886        self.send_request(0x86, axis as u8, home_pin, homing_speed, 0)?;
887        Ok(())
888    }
889
890    /// Configure axis limits
891    pub fn configure_axis_limits(
892        &mut self,
893        axis: usize,
894        limit_n_pin: u8,
895        limit_p_pin: u8,
896        soft_limit_min: i32,
897        soft_limit_max: i32,
898    ) -> Result<()> {
899        if axis >= 8 {
900            return Err(PoKeysError::Parameter("Invalid axis number".to_string()));
901        }
902
903        self.pulse_engine_v2.pin_limit_m_switch[axis] = limit_n_pin;
904        self.pulse_engine_v2.pin_limit_p_switch[axis] = limit_p_pin;
905        self.pulse_engine_v2.soft_limit_minimum[axis] = soft_limit_min;
906        self.pulse_engine_v2.soft_limit_maximum[axis] = soft_limit_max;
907
908        self.send_request(0x87, axis as u8, limit_n_pin, limit_p_pin, 0)?;
909        Ok(())
910    }
911
912    /// Emergency stop
913    pub fn emergency_stop(&mut self) -> Result<()> {
914        self.send_request(0x88, 0, 0, 0, 0)?;
915        Ok(())
916    }
917
918    /// Read pulse engine status
919    pub fn read_pulse_engine_status(&mut self) -> Result<()> {
920        let response = self.send_request(0x89, 0, 0, 0, 0)?;
921
922        // Parse pulse engine status from response
923        if response.len() >= 64 {
924            self.pulse_engine_v2.pulse_engine_state = response[8];
925            self.pulse_engine_v2.axis_enabled_mask = response[9];
926            self.pulse_engine_v2.limit_status_p = response[10];
927            self.pulse_engine_v2.limit_status_n = response[11];
928            self.pulse_engine_v2.home_status = response[12];
929
930            // Parse axis positions
931            for i in 0..8 {
932                let pos_index = 16 + (i * 4);
933                if pos_index + 3 < response.len() {
934                    self.pulse_engine_v2.current_position[i] = i32::from_le_bytes([
935                        response[pos_index],
936                        response[pos_index + 1],
937                        response[pos_index + 2],
938                        response[pos_index + 3],
939                    ]);
940                }
941            }
942        }
943
944        Ok(())
945    }
946
947    /// Get pulse engine state
948    pub fn get_pulse_engine_state(&mut self) -> Result<PulseEngineState> {
949        self.read_pulse_engine_status()?;
950        Ok(self.pulse_engine_v2.get_state())
951    }
952
953    /// Get axis state
954    pub fn get_axis_state(&mut self, axis: usize) -> Result<PulseEngineAxisState> {
955        if axis >= 8 {
956            return Err(PoKeysError::Parameter("Invalid axis number".to_string()));
957        }
958
959        self.read_pulse_engine_status()?;
960        Ok(self.pulse_engine_v2.get_axis_state(axis))
961    }
962
963    /// Check if axis is homed
964    pub fn is_axis_homed(&mut self, axis: usize) -> Result<bool> {
965        if axis >= 8 {
966            return Err(PoKeysError::Parameter("Invalid axis number".to_string()));
967        }
968
969        self.read_pulse_engine_status()?;
970        Ok(self.pulse_engine_v2.is_axis_homed(axis))
971    }
972
973    /// Wait for axis to complete movement
974    pub fn wait_for_axis(&mut self, axis: usize, timeout_ms: u32) -> Result<()> {
975        if axis >= 8 {
976            return Err(PoKeysError::Parameter("Invalid axis number".to_string()));
977        }
978
979        let start_time = std::time::Instant::now();
980        let timeout = std::time::Duration::from_millis(timeout_ms as u64);
981
982        loop {
983            let state = self.get_axis_state(axis)?;
984
985            match state {
986                PulseEngineAxisState::Ready | PulseEngineAxisState::Stopped => break,
987                PulseEngineAxisState::Error | PulseEngineAxisState::Limit => {
988                    return Err(PoKeysError::Protocol(
989                        "Axis error or limit reached".to_string(),
990                    ));
991                }
992                _ => {
993                    if start_time.elapsed() > timeout {
994                        return Err(PoKeysError::Timeout);
995                    }
996                    std::thread::sleep(std::time::Duration::from_millis(10));
997                }
998            }
999        }
1000
1001        Ok(())
1002    }
1003
1004    /// Set internal motor drivers configuration (0x85/0x19)
1005    pub fn set_motor_drivers_configuration(&mut self) -> Result<()> {
1006        let mut request = vec![0u8; 56]; // Data payload (protocol bytes 9-64)
1007
1008        // Set motor driver settings for each axis (following official PoKeysLib)
1009        // device->request[8 + i*2] = pe->InternalDriverStepConfig[i];
1010        // device->request[9 + i*2] = pe->InternalDriverCurrentConfig[i];
1011        // But our request starts at protocol byte 9, so we need offset -1:
1012        // Axis 0: protocol bytes 8,9 -> request[-1,0] (invalid)
1013        // Axis 1: protocol bytes 10,11 -> request[1,2]
1014        // Axis 2: protocol bytes 12,13 -> request[3,4]
1015        // Axis 3: protocol bytes 14,15 -> request[5,6]
1016
1017        for axis in 0..4 {
1018            let protocol_offset = 9 + axis * 2; // Changed from 8 to 9
1019            if protocol_offset >= 9 {
1020                // Only if within our request range (starts at byte 9)
1021                let request_offset = protocol_offset - 9; // Convert to request array index
1022                if request_offset + 1 < request.len() {
1023                    request[request_offset] = self.pulse_engine_v2.motor_step_setting[axis];
1024                    request[request_offset + 1] = self.pulse_engine_v2.motor_current_setting[axis];
1025                }
1026            }
1027        }
1028
1029        println!(
1030            "DEBUG 0x85/0x19 REQUEST: Axis 2 -> protocol bytes 13,14 -> request[4,5] = step={}, current={}",
1031            request[4], request[5]
1032        );
1033
1034        let response = self.send_request_with_data(0x85, 0x19, 0, 0, 0, &request)?;
1035
1036        println!(
1037            "DEBUG 0x85/0x19 RESPONSE: protocol bytes 13,14 = step={}, current={}",
1038            response.get(13).unwrap_or(&255),
1039            response.get(14).unwrap_or(&255)
1040        );
1041
1042        Ok(())
1043    }
1044
1045    /// Create axis configuration builder
1046    pub fn configure_axis(&mut self, axis: usize) -> AxisConfigBuilder {
1047        AxisConfigBuilder::new(axis)
1048    }
1049
1050    /// Set axis positions (0x85/0x03)
1051    pub fn set_axis_positions(&mut self, axis_mask: u8, positions: &[i32; 8]) -> Result<()> {
1052        let mut request = vec![0u8; 56]; // Data payload (protocol bytes 9-64)
1053
1054        // Bytes 0-31: Axis positions (8x 32-bit integers, LSB first)
1055        for (i, &position) in positions.iter().enumerate() {
1056            let pos_bytes = position.to_le_bytes();
1057            let offset = i * 4;
1058            request[offset..offset + 4].copy_from_slice(&pos_bytes);
1059        }
1060
1061        self.send_request_with_data(0x85, 0x03, axis_mask, 0, 0, &request)?;
1062        Ok(())
1063    }
1064
1065    /// Move PV (Set reference position and speed) (0x85/0x25)
1066    pub fn move_pv(&mut self, axis_mask: u8, positions: &[i32; 8], velocity: u16) -> Result<()> {
1067        let mut request = vec![0u8; 56]; // Data payload (protocol bytes 9-64)
1068
1069        // Bytes 0-31: Reference positions (8x 32-bit integers, LSB first)
1070        for (i, &position) in positions.iter().enumerate() {
1071            let pos_bytes = position.to_le_bytes();
1072            let offset = i * 4;
1073            request[offset..offset + 4].copy_from_slice(&pos_bytes);
1074        }
1075
1076        // Bytes 32-47: Move velocity (16 bytes, but only first 2 used)
1077        let vel_bytes = velocity.to_le_bytes();
1078        request[32..34].copy_from_slice(&vel_bytes);
1079
1080        self.send_request_with_data(0x85, 0x25, axis_mask, 0, 0, &request)?;
1081        Ok(())
1082    }
1083
1084    /// Create motor driver configuration builder
1085    pub fn configure_motor_drivers(&mut self) -> MotorDriverConfigBuilder {
1086        MotorDriverConfigBuilder::new()
1087    }
1088
1089    /// Get internal motor drivers configuration (0x85/0x18)
1090    pub fn get_motor_drivers_configuration(&mut self) -> Result<()> {
1091        let response = self.send_request(0x85, 0x18, 0, 0, 0)?;
1092
1093        // Parse motor driver settings for each axis
1094        for axis in 0..4 {
1095            let byte_offset = 8 + (axis * 2); // Bytes 9-16 for axes 1-4
1096            if byte_offset + 1 < response.len() {
1097                let step_setting = response[byte_offset];
1098                let current_setting = response[byte_offset + 1];
1099
1100                // Device returns 0-based values which match our 0x85/0x19 command format
1101                self.pulse_engine_v2.motor_step_setting[axis] = step_setting;
1102                self.pulse_engine_v2.motor_current_setting[axis] = current_setting;
1103            }
1104        }
1105
1106        Ok(())
1107    }
1108}
1109
1110/// Axis configuration builder
1111pub struct AxisConfigBuilder {
1112    axis: usize,
1113    max_speed: f32,
1114    max_acceleration: f32,
1115    max_deceleration: f32,
1116    soft_limit_min: i32,
1117    soft_limit_max: i32,
1118}
1119
1120impl AxisConfigBuilder {
1121    pub fn new(axis: usize) -> Self {
1122        Self {
1123            axis,
1124            max_speed: 1000.0,
1125            max_acceleration: 100.0,
1126            max_deceleration: 100.0,
1127            soft_limit_min: 0,
1128            soft_limit_max: 0,
1129        }
1130    }
1131
1132    pub fn max_speed(mut self, speed: f32) -> Self {
1133        self.max_speed = speed;
1134        self
1135    }
1136
1137    pub fn max_acceleration(mut self, acceleration: f32) -> Self {
1138        self.max_acceleration = acceleration;
1139        self
1140    }
1141
1142    pub fn max_deceleration(mut self, deceleration: f32) -> Self {
1143        self.max_deceleration = deceleration;
1144        self
1145    }
1146
1147    pub fn soft_limit_min(mut self, min: i32) -> Self {
1148        self.soft_limit_min = min;
1149        self
1150    }
1151
1152    pub fn soft_limit_max(mut self, max: i32) -> Self {
1153        self.soft_limit_max = max;
1154        self
1155    }
1156
1157    pub fn build(self, device: &mut PoKeysDevice) -> Result<()> {
1158        if self.axis >= 8 {
1159            return Err(PoKeysError::Parameter("Axis index must be 0-7".to_string()));
1160        }
1161
1162        // Store values directly as floats (no conversion needed)
1163        device.pulse_engine_v2.max_speed[self.axis] = self.max_speed;
1164        device.pulse_engine_v2.max_acceleration[self.axis] = self.max_acceleration;
1165        device.pulse_engine_v2.max_deceleration[self.axis] = self.max_deceleration;
1166        device.pulse_engine_v2.soft_limit_minimum[self.axis] = self.soft_limit_min;
1167        device.pulse_engine_v2.soft_limit_maximum[self.axis] = self.soft_limit_max;
1168
1169        device.set_axis_configuration(self.axis)
1170    }
1171}
1172
1173/// Motor driver configuration builder
1174pub struct MotorDriverConfigBuilder {
1175    step_settings: [Option<u8>; 4],
1176    current_settings: [Option<u8>; 4],
1177}
1178
1179impl Default for MotorDriverConfigBuilder {
1180    fn default() -> Self {
1181        Self::new()
1182    }
1183}
1184
1185impl MotorDriverConfigBuilder {
1186    pub fn new() -> Self {
1187        Self {
1188            step_settings: [None; 4],
1189            current_settings: [None; 4],
1190        }
1191    }
1192
1193    pub fn axis_step_setting(mut self, axis: usize, step_setting: u8) -> Self {
1194        if axis < 4 {
1195            self.step_settings[axis] = Some(step_setting);
1196        }
1197        self
1198    }
1199
1200    pub fn axis_current_setting(mut self, axis: usize, current_setting: u8) -> Self {
1201        if axis < 4 {
1202            self.current_settings[axis] = Some(current_setting);
1203        }
1204        self
1205    }
1206
1207    pub fn build(self, device: &mut PoKeysDevice) -> Result<()> {
1208        // Only update the values that were explicitly set
1209        for axis in 0..4 {
1210            if let Some(step_setting) = self.step_settings[axis] {
1211                device.pulse_engine_v2.motor_step_setting[axis] = step_setting;
1212            }
1213            if let Some(current_setting) = self.current_settings[axis] {
1214                device.pulse_engine_v2.motor_current_setting[axis] = current_setting;
1215            }
1216        }
1217        device.set_motor_drivers_configuration()
1218    }
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223    use super::*;
1224
1225    #[test]
1226    fn test_pulse_engine_creation() {
1227        let pe = PulseEngineV2::new();
1228        assert!(!pe.is_enabled());
1229        assert!(!pe.is_activated());
1230        assert_eq!(pe.get_state(), PulseEngineState::Stopped);
1231    }
1232
1233    #[test]
1234    fn test_axis_state_conversion() {
1235        let mut pe = PulseEngineV2::new();
1236
1237        pe.axes_state[0] = 1;
1238        assert_eq!(pe.get_axis_state(0), PulseEngineAxisState::Ready);
1239
1240        pe.axes_state[0] = 2;
1241        assert_eq!(pe.get_axis_state(0), PulseEngineAxisState::Running);
1242
1243        pe.axes_state[0] = 20;
1244        assert_eq!(pe.get_axis_state(0), PulseEngineAxisState::Error);
1245    }
1246
1247    #[test]
1248    fn test_axis_enable_mask() {
1249        let mut pe = PulseEngineV2::new();
1250
1251        assert!(!pe.is_axis_enabled(0));
1252
1253        pe.axis_enabled_mask = 0b00000001;
1254        assert!(pe.is_axis_enabled(0));
1255        assert!(!pe.is_axis_enabled(1));
1256
1257        pe.axis_enabled_mask = 0b00000011;
1258        assert!(pe.is_axis_enabled(0));
1259        assert!(pe.is_axis_enabled(1));
1260        assert!(!pe.is_axis_enabled(2));
1261    }
1262}