674 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Axis processing for CMDR Joystick 25
//!
//! Responsibilities
//! - Apply gimbal mode compensation (M10/M7) to raw ADC readings
//! - Feed smoothed samples into peraxis filters
//! - Calibrate + apply deadzones + optional expo via lookup table
//! - Implement throttlehold remapping on the throttle axis
//! - Maintain virtual axes updated by button presses
//! - Detect activity to throttle USB traffic
//!
//! Data flow
//! raw ADC → gimbal compensation → smoothing → `GimbalAxis::process_value`
//! → throttle hold → activity detection → USB report conversion
use crate::buttons::{Button, TOTAL_BUTTONS};
use crate::expo::{constrain, ExpoLUT};
use crate::hardware::{ADC_MAX, ADC_MIN, AXIS_CENTER, NBR_OF_GIMBAL_AXIS};
use crate::mapping::{
BUTTON_FRONT_LEFT_EXTRA, BUTTON_FRONT_LEFT_LOWER, BUTTON_FRONT_LEFT_UPPER,
BUTTON_FRONT_RIGHT_EXTRA,
};
use dyn_smooth::{DynamicSmootherEcoI32, I32_FRAC_BITS};
// ==================== AXIS CONSTANTS ====================
pub const GIMBAL_AXIS_LEFT_X: usize = 0;
pub const GIMBAL_AXIS_LEFT_Y: usize = 1;
pub const GIMBAL_AXIS_RIGHT_X: usize = 2;
pub const GIMBAL_AXIS_RIGHT_Y: usize = 3;
pub const GIMBAL_MODE_M10: u8 = 0;
pub const GIMBAL_MODE_M7: u8 = 1;
/// Digital smoothing parameters for `DynamicSmootherEcoI32` filters.
/// Reduce ADC noise and jitter for stable axis values.
pub const BASE_FREQ: i32 = 2 << I32_FRAC_BITS;
pub const SAMPLE_FREQ: i32 = 1000 << I32_FRAC_BITS;
pub const SENSITIVITY: i32 = (0.01 * ((1 << I32_FRAC_BITS) as f32)) as i32;
// ==================== AXIS STRUCTS ====================
#[derive(Copy, Clone)]
pub struct GimbalAxis {
pub value: u16,
pub value_before_hold: u16,
pub previous_value: u16,
pub max: u16,
pub min: u16,
pub center: u16,
pub deadzone: (u16, u16, u16),
pub expo: bool,
pub hold: u16,
pub hold_pending: bool,
}
impl Default for GimbalAxis {
fn default() -> Self {
GimbalAxis {
value: AXIS_CENTER,
value_before_hold: AXIS_CENTER,
previous_value: AXIS_CENTER,
max: ADC_MAX,
min: ADC_MIN,
center: AXIS_CENTER,
deadzone: (100, 50, 100),
expo: true,
hold: AXIS_CENTER,
hold_pending: false,
}
}
}
impl GimbalAxis {
/// Create a new GimbalAxis with default settings.
pub fn new() -> Self {
Self::default()
}
/// Create a new GimbalAxis with calibration data.
pub fn with_calibration(min: u16, max: u16, center: u16) -> Self {
let mut axis = Self::new();
axis.set_calibration(min, max, center);
axis
}
/// Apply calibration data to the axis.
pub fn set_calibration(&mut self, min: u16, max: u16, center: u16) {
self.min = min;
self.max = max;
self.center = center;
}
/// Process filtered ADC value through calibration, deadzone and optional expo.
pub fn process_value(&mut self, filtered_value: i32, expo_lut: &ExpoLUT) {
// Convert filtered value to u16 range
let raw_value = constrain(filtered_value, ADC_MIN as i32, ADC_MAX as i32) as u16;
// Apply calibration and expo processing
self.value = calculate_axis_value(
raw_value,
self.min,
self.max,
self.center,
self.deadzone,
self.expo,
expo_lut,
);
self.value_before_hold = self.value;
}
/// Set/arm a hold value for the axis (used by throttle hold).
pub fn set_hold(&mut self, value: u16) {
self.hold = value;
self.hold_pending = true;
}
/// Check if the axis value changed since the last probe (activity flag).
pub fn check_activity(&mut self) -> bool {
let activity = self.value != self.previous_value;
self.previous_value = self.value;
activity
}
/// Throttlehold remapping for throttle axis (center → hold; below/above remapped).
pub fn process_throttle_hold(&mut self, throttle_hold_enable: bool) {
if throttle_hold_enable && self.value < AXIS_CENTER && !self.hold_pending {
self.value = remap(self.value, ADC_MIN, AXIS_CENTER, ADC_MIN, self.hold);
} else if throttle_hold_enable && self.value > AXIS_CENTER && !self.hold_pending {
self.value = remap(self.value, AXIS_CENTER, ADC_MAX, self.hold, ADC_MAX);
} else if throttle_hold_enable && self.value == AXIS_CENTER {
self.value = self.hold;
self.hold_pending = false;
} else if throttle_hold_enable {
self.value = self.hold;
}
}
}
#[derive(Copy, Clone)]
pub struct VirtualAxis {
pub value: u16,
step: u16,
}
impl Default for VirtualAxis {
fn default() -> Self {
VirtualAxis {
value: AXIS_CENTER,
step: 5,
}
}
}
impl VirtualAxis {
pub fn new(step: u16) -> Self {
VirtualAxis {
value: AXIS_CENTER,
step,
}
}
/// Update virtual axis based on button inputs.
/// Returns true if USB activity should be signaled.
pub fn update(&mut self, up_pressed: bool, down_pressed: bool, _vt_enable: bool) -> bool {
let mut activity = false;
// Compensate value when changing direction
if up_pressed && !down_pressed && self.value < AXIS_CENTER {
self.value = AXIS_CENTER + (AXIS_CENTER - self.value) / 2;
} else if down_pressed && !up_pressed && self.value > AXIS_CENTER {
self.value = AXIS_CENTER - (self.value - AXIS_CENTER) / 2;
}
// Move virtual axis
if up_pressed && !down_pressed && self.value < ADC_MAX - self.step {
self.value += self.step;
activity = true;
} else if down_pressed && !up_pressed && self.value > ADC_MIN + self.step {
self.value -= self.step;
activity = true;
} else if (self.value != AXIS_CENTER && !up_pressed && !down_pressed)
|| (up_pressed && down_pressed)
{
// Return to center when no buttons are pressed or both are pressed
let before = self.value;
if self.value > AXIS_CENTER {
self.value = self.value.saturating_sub(self.step);
} else if self.value < AXIS_CENTER {
self.value = self.value.saturating_add(self.step);
}
activity |= self.value != before;
}
activity
}
}
// ==================== AXIS MANAGER ====================
pub struct AxisManager {
pub axes: [GimbalAxis; NBR_OF_GIMBAL_AXIS],
pub virtual_ry: VirtualAxis,
pub virtual_rz: VirtualAxis,
pub gimbal_mode: u8,
pub throttle_hold_enable: bool,
}
impl Default for AxisManager {
fn default() -> Self {
Self::new()
}
}
impl AxisManager {
pub fn new() -> Self {
Self {
axes: [GimbalAxis::new(); NBR_OF_GIMBAL_AXIS],
virtual_ry: VirtualAxis::new(5),
virtual_rz: VirtualAxis::new(5),
gimbal_mode: GIMBAL_MODE_M10,
throttle_hold_enable: false,
}
}
pub fn set_gimbal_mode(&mut self, mode: u8) {
self.gimbal_mode = mode;
}
/// Apply gimbal mode compensation to raw ADC values.
pub fn apply_gimbal_compensation(&self, raw_values: &mut [u16; 4]) {
if self.gimbal_mode == GIMBAL_MODE_M10 {
// Invert X1 and Y2 axis (M10 gimbals)
raw_values[GIMBAL_AXIS_LEFT_X] = ADC_MAX - raw_values[GIMBAL_AXIS_LEFT_X];
raw_values[GIMBAL_AXIS_RIGHT_Y] = ADC_MAX - raw_values[GIMBAL_AXIS_RIGHT_Y];
} else if self.gimbal_mode == GIMBAL_MODE_M7 {
// Invert Y1 and X2 axis (M7 gimbals)
raw_values[GIMBAL_AXIS_LEFT_Y] = ADC_MAX - raw_values[GIMBAL_AXIS_LEFT_Y];
raw_values[GIMBAL_AXIS_RIGHT_X] = ADC_MAX - raw_values[GIMBAL_AXIS_RIGHT_X];
}
}
/// Tick smoothing filters with latest raw samples.
pub fn update_smoothers(
&self,
smoother: &mut [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
raw_values: &[u16; 4],
) {
smoother[GIMBAL_AXIS_LEFT_X].tick(raw_values[GIMBAL_AXIS_LEFT_X] as i32);
smoother[GIMBAL_AXIS_LEFT_Y].tick(raw_values[GIMBAL_AXIS_LEFT_Y] as i32);
smoother[GIMBAL_AXIS_RIGHT_X].tick(raw_values[GIMBAL_AXIS_RIGHT_X] as i32);
smoother[GIMBAL_AXIS_RIGHT_Y].tick(raw_values[GIMBAL_AXIS_RIGHT_Y] as i32);
}
/// Update all axes from smoothers (calibration, deadzone, expo) and apply holds.
pub fn process_axis_values(
&mut self,
smoother: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
expo_lut: &ExpoLUT,
) -> bool {
for (index, axis) in self.axes.iter_mut().enumerate() {
axis.process_value(smoother[index].value(), expo_lut);
}
self.process_throttle_hold();
self.check_activity()
}
/// Apply throttlehold to the leftY axis and derive enable flag.
pub fn process_throttle_hold(&mut self) {
self.throttle_hold_enable = self.axes[GIMBAL_AXIS_LEFT_Y].hold != AXIS_CENTER;
self.axes[GIMBAL_AXIS_LEFT_Y].process_throttle_hold(self.throttle_hold_enable);
}
/// Clear throttlehold state for leftY axis (reset to initial state).
pub fn clear_throttle_hold(&mut self) {
self.axes[GIMBAL_AXIS_LEFT_Y].hold = AXIS_CENTER;
self.axes[GIMBAL_AXIS_LEFT_Y].hold_pending = false;
}
/// Update virtual axes based on button inputs.
/// Returns true if USB activity should be signaled.
pub fn update_virtual_axes(
&mut self,
buttons: &[Button; TOTAL_BUTTONS],
vt_enable: bool,
) -> bool {
let mut activity = false;
// Update Virtual RY
let ry_activity = self.virtual_ry.update(
buttons[BUTTON_FRONT_LEFT_UPPER].pressed,
buttons[BUTTON_FRONT_LEFT_LOWER].pressed,
vt_enable,
);
if ry_activity {
activity = true;
}
// Update Virtual RZ
let rz_activity = self.virtual_rz.update(
buttons[BUTTON_FRONT_RIGHT_EXTRA].pressed,
buttons[BUTTON_FRONT_LEFT_EXTRA].pressed,
vt_enable,
);
if rz_activity {
activity = true;
}
activity
}
/// Check for axis activity (movement from previous value).
pub fn check_activity(&mut self) -> bool {
let mut activity = false;
for axis in self.axes.iter_mut() {
if axis.check_activity() {
activity = true;
}
}
activity
}
/// Set throttlehold value for leftY axis (original behavior).
pub fn set_throttle_hold(&mut self, hold_value: u16) {
self.axes[GIMBAL_AXIS_LEFT_Y].set_hold(hold_value);
}
/// Get prehold value for special button combinations (original logic).
pub fn get_value_before_hold(&self) -> u16 {
// Original code captured axis.value BEFORE throttle hold was applied
self.axes[GIMBAL_AXIS_LEFT_Y].value_before_hold
}
/// Get virtual RY axis value for joystick report.
pub fn get_virtual_ry_value(&self, expo_lut: &ExpoLUT) -> u16 {
calculate_axis_value(
self.virtual_ry.value,
ADC_MIN,
ADC_MAX,
AXIS_CENTER,
(0, 0, 0),
true,
expo_lut,
)
}
/// Get virtual RZ axis value for joystick report.
pub fn get_virtual_rz_value(&self, expo_lut: &ExpoLUT) -> u16 {
calculate_axis_value(
self.virtual_rz.value,
ADC_MIN,
ADC_MAX,
AXIS_CENTER,
(0, 0, 0),
true,
expo_lut,
)
}
/// Initialize digital smoothing filters for each gimbal axis.
/// Creates an array of `DynamicSmootherEcoI32` filters configured with shared DSP parameters.
pub fn create_smoothers() -> [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS] {
[
DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
]
}
}
// ==================== AXIS PROCESSING FUNCTIONS ====================
/// Remap a value from one range to another using integer math and clamping.
///
/// # Arguments
/// * `value` - Value to remap
/// * `in_min` - Lower bound of the value's current range
/// * `in_max` - Upper bound of the value's current range
/// * `out_min` - Lower bound of the value's target range
/// * `out_max` - Upper bound of the value's target range
pub fn remap(value: u16, in_min: u16, in_max: u16, out_min: u16, out_max: u16) -> u16 {
constrain(
(value as i64 - in_min as i64) * (out_max as i64 - out_min as i64)
/ (in_max as i64 - in_min as i64)
+ out_min as i64,
out_min as i64,
out_max as i64,
) as u16
}
/// Calculate calibrated axis value with deadzone and optional expo.
///
/// Process raw input through [min, center, max] calibration, apply deadzones,
/// then apply expo via LUT if enabled for smooth response.
///
/// # Arguments
/// * `value` - Raw axis value to process
/// * `min` - Lower bound of the axis calibrated range
/// * `max` - Upper bound of the axis calibrated range
/// * `center` - Center position of the axis calibrated range
/// * `deadzone` - Deadzone settings (min, center, max) for axis
/// * `expo` - Whether exponential curve is enabled for this axis
/// * `expo_lut` - Exponential curve lookup table for smooth response
///
/// # Returns
/// Processed axis value after calibration, deadzone, and optional expo
pub fn calculate_axis_value(
value: u16,
min: u16,
max: u16,
center: u16,
deadzone: (u16, u16, u16),
expo: bool,
expo_lut: &ExpoLUT,
) -> u16 {
use crate::hardware::{ADC_MAX, ADC_MIN, AXIS_CENTER};
if value <= min {
return ADC_MIN;
}
if value >= max {
return ADC_MAX;
}
let mut calibrated_value = AXIS_CENTER;
if value > (center + deadzone.1) {
calibrated_value = remap(
value,
center + deadzone.1,
max - deadzone.2,
AXIS_CENTER,
ADC_MAX,
);
} else if value < (center - deadzone.1) {
calibrated_value = remap(
value,
min + deadzone.0,
center - deadzone.1,
ADC_MIN,
AXIS_CENTER,
);
}
if expo && calibrated_value != AXIS_CENTER {
calibrated_value = expo_lut.apply(calibrated_value);
}
calibrated_value
}
// ==================== TESTS ====================
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn test_gimbal_axis_default() {
let axis = GimbalAxis::default();
assert_eq!(axis.value, AXIS_CENTER);
assert_eq!(axis.min, ADC_MIN);
assert_eq!(axis.max, ADC_MAX);
assert_eq!(axis.center, AXIS_CENTER);
assert_eq!(axis.hold, AXIS_CENTER);
assert!(!axis.hold_pending);
}
#[test]
fn test_gimbal_axis_new() {
let axis = GimbalAxis::new();
assert_eq!(axis.value, AXIS_CENTER);
assert_eq!(axis.min, ADC_MIN);
assert_eq!(axis.max, ADC_MAX);
assert_eq!(axis.center, AXIS_CENTER);
assert!(!axis.hold_pending);
}
#[test]
fn test_gimbal_axis_with_calibration() {
let axis = GimbalAxis::with_calibration(100, 3900, 2000);
assert_eq!(axis.min, 100);
assert_eq!(axis.max, 3900);
assert_eq!(axis.center, 2000);
}
#[test]
fn test_gimbal_axis_activity_detection() {
let mut axis = GimbalAxis::new();
// Initially no activity (same as previous)
assert!(!axis.check_activity());
// Change value
axis.value = 3000;
assert!(axis.check_activity());
// No change from previous check
assert!(!axis.check_activity());
}
#[test]
fn test_gimbal_axis_throttle_hold_processing() {
let mut axis = GimbalAxis::new();
axis.set_hold(1500); // Set hold value below center
// Test when not held, no processing occurs
axis.hold = AXIS_CENTER;
axis.hold_pending = false;
axis.value = 1000;
axis.process_throttle_hold(true);
assert_eq!(axis.value, 1000); // Should remain unchanged
// Test when held but hold_pending = false, remapping occurs
axis.set_hold(1500);
axis.value = 1000;
axis.hold_pending = false; // This allows remapping
axis.process_throttle_hold(true);
let expected = remap(1000, ADC_MIN, AXIS_CENTER, ADC_MIN, 1500);
assert_eq!(axis.value, expected);
// Test center value gets hold value and clears pending flag
axis.set_hold(1500);
axis.value = AXIS_CENTER;
axis.process_throttle_hold(true);
assert_eq!(axis.value, 1500);
assert!(!axis.hold_pending); // Should clear pending flag
// Test when hold_pending = true, just uses hold value
axis.set_hold(1500);
axis.value = 2000;
axis.hold_pending = true;
axis.process_throttle_hold(true);
assert_eq!(axis.value, 1500);
}
#[test]
fn test_virtual_axis_default() {
let virtual_axis = VirtualAxis::default();
assert_eq!(virtual_axis.value, AXIS_CENTER);
assert_eq!(virtual_axis.step, 5);
}
#[test]
fn test_virtual_axis_movement_up() {
let mut virtual_axis = VirtualAxis::new(10);
// Test upward movement
let activity = virtual_axis.update(true, false, false);
assert!(activity);
assert_eq!(virtual_axis.value, AXIS_CENTER + 10);
}
#[test]
fn test_virtual_axis_movement_down() {
let mut virtual_axis = VirtualAxis::new(10);
// Test downward movement
let activity = virtual_axis.update(false, true, false);
assert!(activity);
assert_eq!(virtual_axis.value, AXIS_CENTER - 10);
}
#[test]
fn test_virtual_axis_return_to_center() {
let mut virtual_axis = VirtualAxis::new(10);
virtual_axis.value = AXIS_CENTER + 20;
// Test return to center when no buttons pressed
let activity = virtual_axis.update(false, false, false);
assert!(activity);
assert_eq!(virtual_axis.value, AXIS_CENTER + 10);
}
#[test]
fn test_virtual_axis_direction_compensation() {
let mut virtual_axis = VirtualAxis::new(10);
virtual_axis.value = AXIS_CENTER - 100;
// Test direction change compensation (includes compensation + movement)
virtual_axis.update(true, false, false);
let compensated = AXIS_CENTER + (AXIS_CENTER - (AXIS_CENTER - 100)) / 2;
let expected = compensated + 10; // Plus step movement
assert_eq!(virtual_axis.value, expected);
}
#[test]
fn test_axis_manager_creation() {
let manager = AxisManager::new();
assert_eq!(manager.axes.len(), NBR_OF_GIMBAL_AXIS);
assert_eq!(manager.gimbal_mode, GIMBAL_MODE_M10);
assert_eq!(manager.virtual_ry.value, AXIS_CENTER);
assert_eq!(manager.virtual_rz.value, AXIS_CENTER);
}
#[test]
fn test_gimbal_compensation_m10() {
let manager = AxisManager::new(); // Default is M10
let mut raw_values = [1000, 1500, 2000, 2500];
manager.apply_gimbal_compensation(&mut raw_values);
// M10 mode inverts X1 and Y2
assert_eq!(raw_values[GIMBAL_AXIS_LEFT_X], ADC_MAX - 1000);
assert_eq!(raw_values[GIMBAL_AXIS_LEFT_Y], 1500); // Not inverted
assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_X], 2000); // Not inverted
assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_Y], ADC_MAX - 2500);
}
#[test]
fn test_gimbal_compensation_m7() {
let mut manager = AxisManager::new();
manager.set_gimbal_mode(GIMBAL_MODE_M7);
let mut raw_values = [1000, 1500, 2000, 2500];
manager.apply_gimbal_compensation(&mut raw_values);
// M7 mode inverts Y1 and X2
assert_eq!(raw_values[GIMBAL_AXIS_LEFT_X], 1000); // Not inverted
assert_eq!(raw_values[GIMBAL_AXIS_LEFT_Y], ADC_MAX - 1500);
assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_X], ADC_MAX - 2000);
assert_eq!(raw_values[GIMBAL_AXIS_RIGHT_Y], 2500); // Not inverted
}
#[test]
fn test_axis_activity_detection() {
let mut manager = AxisManager::new();
// No activity initially
assert!(!manager.check_activity());
// Change value and check activity
manager.axes[0].value = 1000;
assert!(manager.check_activity());
// No activity after previous_value is updated
assert!(!manager.check_activity());
}
#[test]
fn test_calculate_axis_value_boundaries() {
let expo_lut = ExpoLUT::new(0.0); // No expo for testing
// Test min boundary
let result = calculate_axis_value(0, 100, 3000, 2000, (50, 50, 50), false, &expo_lut);
assert_eq!(result, ADC_MIN);
// Test max boundary
let result = calculate_axis_value(4000, 100, 3000, 2000, (50, 50, 50), false, &expo_lut);
assert_eq!(result, ADC_MAX);
}
#[test]
fn test_calculate_axis_value_deadzone() {
let expo_lut = ExpoLUT::new(0.0); // No expo for testing
// Test center deadzone
let result = calculate_axis_value(2000, 100, 3000, 2000, (50, 50, 50), false, &expo_lut);
assert_eq!(result, AXIS_CENTER);
}
#[test]
fn test_remap_function() {
// Test basic remapping
let result = remap(50, 0, 100, 0, 200);
assert_eq!(result, 100);
// Test reverse remapping (constrain limits to out_min when out_min > out_max)
let result = remap(25, 0, 100, 100, 0);
assert_eq!(result, 100);
}
}