use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info};
use evdev::Key;
use crate::analog_calibration::{AnalogCalibration, DeadzoneShape, SensitivityCurve};
const DEFAULT_DEADZONE: u16 = 14000;
pub const MAX_ABS_VALUE: i32 = 32767;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum ResponseCurve {
Linear,
Exponential { exponent: f32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DpadMode {
Disabled,
EightWay,
FourWay,
}
impl Default for DpadMode {
fn default() -> Self {
Self::Disabled
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
impl Default for Direction {
fn default() -> Self {
Self::Up
}
}
const DPAD_UP: u16 = 103; const DPAD_DOWN: u16 = 108; const DPAD_LEFT: u16 = 105; const DPAD_RIGHT: u16 = 106;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DpadDirection {
None,
Up,
UpRight,
Right,
DownRight,
Down,
DownLeft,
Left,
UpLeft,
}
impl Default for DpadDirection {
fn default() -> Self {
Self::None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnalogMode {
Disabled,
Dpad,
Gamepad,
Camera,
Mouse,
Wasd,
}
impl Default for AnalogMode {
fn default() -> Self {
Self::Disabled
}
}
pub fn direction_to_key_code(dir: Direction) -> u16 {
match dir {
Direction::Up => DPAD_UP,
Direction::Down => DPAD_DOWN,
Direction::Left => DPAD_LEFT,
Direction::Right => DPAD_RIGHT,
}
}
pub fn dpad_direction_to_keys(direction: DpadDirection) -> Vec<Key> {
match direction {
DpadDirection::None => vec![],
DpadDirection::Up => vec![Key::KEY_UP],
DpadDirection::Down => vec![Key::KEY_DOWN],
DpadDirection::Left => vec![Key::KEY_LEFT],
DpadDirection::Right => vec![Key::KEY_RIGHT],
DpadDirection::UpLeft => vec![Key::KEY_UP, Key::KEY_LEFT],
DpadDirection::UpRight => vec![Key::KEY_UP, Key::KEY_RIGHT],
DpadDirection::DownLeft => vec![Key::KEY_DOWN, Key::KEY_LEFT],
DpadDirection::DownRight => vec![Key::KEY_DOWN, Key::KEY_RIGHT],
}
}
pub fn wasd_direction_to_keys(direction: DpadDirection) -> Vec<Key> {
match direction {
DpadDirection::None => vec![],
DpadDirection::Up => vec![Key::KEY_W],
DpadDirection::Down => vec![Key::KEY_S],
DpadDirection::Left => vec![Key::KEY_A],
DpadDirection::Right => vec![Key::KEY_D],
DpadDirection::UpLeft => vec![Key::KEY_W, Key::KEY_A],
DpadDirection::UpRight => vec![Key::KEY_W, Key::KEY_D],
DpadDirection::DownLeft => vec![Key::KEY_S, Key::KEY_A],
DpadDirection::DownRight => vec![Key::KEY_S, Key::KEY_D],
}
}
pub fn camera_direction_to_keys(direction: DpadDirection) -> Vec<Key> {
match direction {
DpadDirection::None => vec![],
DpadDirection::Up => vec![Key::KEY_PAGEUP],
DpadDirection::Down => vec![Key::KEY_PAGEDOWN],
DpadDirection::Left => vec![Key::KEY_LEFT],
DpadDirection::Right => vec![Key::KEY_RIGHT],
DpadDirection::UpLeft => vec![Key::KEY_PAGEUP, Key::KEY_LEFT],
DpadDirection::UpRight => vec![Key::KEY_PAGEUP, Key::KEY_RIGHT],
DpadDirection::DownLeft => vec![Key::KEY_PAGEDOWN, Key::KEY_LEFT],
DpadDirection::DownRight => vec![Key::KEY_PAGEDOWN, Key::KEY_RIGHT],
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CameraOutputMode {
Scroll,
Keys,
}
impl Default for CameraOutputMode {
fn default() -> Self {
Self::Scroll }
}
pub enum CameraOutput {
Scroll(i32),
Keys(Vec<Key>),
}
impl Default for ResponseCurve {
fn default() -> Self {
Self::Linear
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceAnalogConfig {
pub device_id: String,
#[serde(default = "default_deadzone")]
pub deadzone: u16,
#[serde(default = "default_sensitivity")]
pub sensitivity: f32,
#[serde(default)]
pub response_curve: ResponseCurve,
#[serde(default)]
pub dpad_mode: DpadMode,
#[serde(default)]
pub mode: AnalogMode,
#[serde(default = "default_deadzone")]
pub inner_deadzone_x: u16,
#[serde(default = "default_deadzone")]
pub inner_deadzone_y: u16,
#[serde(default = "default_outer_deadzone")]
pub outer_deadzone_x: u16,
#[serde(default = "default_outer_deadzone")]
pub outer_deadzone_y: u16,
}
fn default_deadzone() -> u16 {
DEFAULT_DEADZONE
}
fn default_sensitivity() -> f32 {
1.0
}
fn default_outer_deadzone() -> u16 {
MAX_ABS_VALUE as u16 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MouseVelocityConfig {
#[serde(default = "default_mouse_multiplier")]
pub multiplier: f32,
}
fn default_mouse_multiplier() -> f32 {
10.0 }
impl Default for MouseVelocityConfig {
fn default() -> Self {
Self {
multiplier: default_mouse_multiplier(),
}
}
}
pub fn default_mouse_velocity_config() -> MouseVelocityConfig {
MouseVelocityConfig {
multiplier: 10.0, }
}
impl DeviceAnalogConfig {
pub fn new(device_id: String) -> Self {
Self {
device_id,
deadzone: DEFAULT_DEADZONE,
sensitivity: 1.0,
response_curve: ResponseCurve::Linear,
dpad_mode: DpadMode::Disabled,
mode: AnalogMode::Disabled,
inner_deadzone_x: DEFAULT_DEADZONE,
inner_deadzone_y: DEFAULT_DEADZONE,
outer_deadzone_x: MAX_ABS_VALUE as u16,
outer_deadzone_y: MAX_ABS_VALUE as u16,
}
}
pub fn with_deadzone(device_id: String, deadzone: u16) -> Self {
Self {
device_id,
deadzone,
sensitivity: 1.0,
response_curve: ResponseCurve::Linear,
dpad_mode: DpadMode::Disabled,
mode: AnalogMode::Disabled,
inner_deadzone_x: deadzone,
inner_deadzone_y: deadzone,
outer_deadzone_x: MAX_ABS_VALUE as u16,
outer_deadzone_y: MAX_ABS_VALUE as u16,
}
}
}
pub struct AnalogProcessor {
devices: Arc<RwLock<HashMap<String, DeviceAnalogConfig>>>,
}
impl AnalogProcessor {
pub fn new() -> Self {
Self {
devices: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn process_event(
&self,
device_id: &str,
axis_code: u16,
raw_value: i32,
) -> Option<i32> {
let config = self.get_or_create_device_config(device_id).await;
let (inner_deadzone, outer_deadzone) = match axis_code {
61000 => (config.inner_deadzone_x as i32, config.outer_deadzone_x as i32), 61001 => (config.inner_deadzone_y as i32, config.outer_deadzone_y as i32), _ => (config.deadzone as i32, MAX_ABS_VALUE), };
if raw_value.abs() < inner_deadzone {
debug!(
"Analog event filtered by inner deadzone: device={}, axis={}, value={}, inner_deadzone={}",
device_id, axis_code, raw_value, inner_deadzone
);
return None;
}
let abs_value = raw_value.abs().min(outer_deadzone);
let sign = raw_value.signum();
let normalized = ((abs_value - inner_deadzone) as f32 / (outer_deadzone - inner_deadzone) as f32)
.clamp(0.0, 1.0);
let scaled = normalized * config.sensitivity;
let output = match config.response_curve {
ResponseCurve::Linear => {
scaled
}
ResponseCurve::Exponential { exponent } => {
scaled.powf(exponent.clamp(0.1, 5.0))
}
};
let final_value = (sign as f32 * output * MAX_ABS_VALUE as f32) as i32;
debug!(
"Analog event processed: device={}, axis={}, raw={}, inner_deadzone={}, outer_deadzone={}, sensitivity={:.2}, curve={:?}, output={}",
device_id, axis_code, raw_value, inner_deadzone, outer_deadzone, config.sensitivity, config.response_curve, final_value
);
Some(final_value)
}
pub async fn set_deadzone(&self, device_id: &str, value: u16) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.deadzone = value;
config.inner_deadzone_x = value;
config.inner_deadzone_y = value;
info!(
"Deadzone updated (both axes): device={}, deadzone={}",
device_id, value
);
}
pub async fn set_deadzone_x(&self, device_id: &str, value: u16) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.inner_deadzone_x = value;
info!(
"X-axis deadzone updated: device={}, deadzone_x={}",
device_id, value
);
}
pub async fn set_deadzone_y(&self, device_id: &str, value: u16) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.inner_deadzone_y = value;
info!(
"Y-axis deadzone updated: device={}, deadzone_y={}",
device_id, value
);
}
pub async fn set_deadzone_percentage(&self, device_id: &str, percentage: u8) -> Result<(), String> {
if percentage > 100 {
return Err(format!(
"Invalid deadzone percentage: {} (must be 0-100)",
percentage
));
}
let raw_value = (percentage as u32 * MAX_ABS_VALUE as u32 / 100) as u16;
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.deadzone = raw_value;
config.inner_deadzone_x = raw_value;
config.inner_deadzone_y = raw_value;
info!(
"Deadzone updated via percentage (both axes): device={}, {}% = {} raw",
device_id, percentage, raw_value
);
Ok(())
}
pub async fn set_deadzone_percentage_x(&self, device_id: &str, percentage: u8) -> Result<(), String> {
if percentage > 100 {
return Err(format!("Invalid deadzone percentage: {}", percentage));
}
let raw_value = (percentage as u32 * MAX_ABS_VALUE as u32 / 100) as u16;
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.inner_deadzone_x = raw_value;
info!("X-axis deadzone updated: device={}, {}% = {} raw", device_id, percentage, raw_value);
Ok(())
}
pub async fn set_deadzone_percentage_y(&self, device_id: &str, percentage: u8) -> Result<(), String> {
if percentage > 100 {
return Err(format!("Invalid deadzone percentage: {}", percentage));
}
let raw_value = (percentage as u32 * MAX_ABS_VALUE as u32 / 100) as u16;
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.inner_deadzone_y = raw_value;
info!("Y-axis deadzone updated: device={}, {}% = {} raw", device_id, percentage, raw_value);
Ok(())
}
pub async fn get_deadzone_percentage(&self, device_id: &str) -> u8 {
self.get_deadzone_percentage_x(device_id).await
}
pub async fn get_deadzone_percentage_x(&self, device_id: &str) -> u8 {
let devices = self.devices.read().await;
if let Some(config) = devices.get(device_id) {
(config.inner_deadzone_x as u32 * 100 / MAX_ABS_VALUE as u32) as u8
} else {
(DEFAULT_DEADZONE as u32 * 100 / MAX_ABS_VALUE as u32) as u8
}
}
pub async fn get_deadzone_percentage_y(&self, device_id: &str) -> u8 {
let devices = self.devices.read().await;
if let Some(config) = devices.get(device_id) {
(config.inner_deadzone_y as u32 * 100 / MAX_ABS_VALUE as u32) as u8
} else {
(DEFAULT_DEADZONE as u32 * 100 / MAX_ABS_VALUE as u32) as u8
}
}
pub async fn set_outer_deadzone_x(&self, device_id: &str, value: u16) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.outer_deadzone_x = value;
info!(
"X-axis outer deadzone updated: device={}, outer_deadzone_x={}",
device_id, value
);
}
pub async fn set_outer_deadzone_y(&self, device_id: &str, value: u16) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.outer_deadzone_y = value;
info!(
"Y-axis outer deadzone updated: device={}, outer_deadzone_y={}",
device_id, value
);
}
pub async fn get_outer_deadzone_percentage_x(&self, device_id: &str) -> u8 {
let devices = self.devices.read().await;
if let Some(config) = devices.get(device_id) {
(config.outer_deadzone_x as u32 * 100 / MAX_ABS_VALUE as u32) as u8
} else {
100 }
}
pub async fn get_outer_deadzone_percentage_y(&self, device_id: &str) -> u8 {
let devices = self.devices.read().await;
if let Some(config) = devices.get(device_id) {
(config.outer_deadzone_y as u32 * 100 / MAX_ABS_VALUE as u32) as u8
} else {
100 }
}
pub async fn set_sensitivity(&self, device_id: &str, value: f32) {
let clamped = value.clamp(0.1, 5.0);
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.sensitivity = clamped;
info!(
"Sensitivity updated: device={}, sensitivity={:.2}",
device_id, clamped
);
}
pub async fn set_response_curve(&self, device_id: &str, curve: ResponseCurve) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.response_curve = curve;
info!(
"Response curve updated: device={}, curve={:?}",
device_id, curve
);
}
pub async fn set_calibration(&self, device_id: &str, layer_id: usize, calibration: AnalogCalibration) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
if layer_id == 0 {
config.deadzone = (calibration.deadzone * 32767.0) as u16;
config.sensitivity = calibration.sensitivity_multiplier;
config.response_curve = match calibration.sensitivity {
SensitivityCurve::Linear => ResponseCurve::Linear,
SensitivityCurve::Quadratic => ResponseCurve::Exponential { exponent: 2.0 },
SensitivityCurve::Exponential { exponent } => ResponseCurve::Exponential { exponent },
};
info!(
"Calibration updated: device={}, deadzone={:.2}, sensitivity={:.2}, curve={:?}",
device_id, calibration.deadzone, calibration.sensitivity_multiplier, calibration.sensitivity
);
} else {
debug!(
"Layer-specific calibration not yet supported, storing for layer {}",
layer_id
);
}
}
pub async fn get_or_create_device_config(&self, device_id: &str) -> DeviceAnalogConfig {
let devices = self.devices.read().await;
if let Some(config) = devices.get(device_id) {
config.clone()
} else {
drop(devices);
let mut devices = self.devices.write().await;
devices
.entry(device_id.to_string())
.or_insert_with(|| DeviceAnalogConfig::new(device_id.to_string()))
.clone()
}
}
pub async fn get_device_config(&self, device_id: &str) -> Option<DeviceAnalogConfig> {
let devices = self.devices.read().await;
devices.get(device_id).cloned()
}
pub async fn remove_device_config(&self, device_id: &str) {
let mut devices = self.devices.write().await;
if devices.remove(device_id).is_some() {
info!("Device config removed: {}", device_id);
}
}
pub async fn get_configured_devices(&self) -> Vec<String> {
let devices = self.devices.read().await;
devices.keys().cloned().collect()
}
pub async fn set_dpad_mode(&self, device_id: &str, mode: DpadMode) {
let mut devices = self.devices.write().await;
let config = devices.entry(device_id.to_string()).or_insert_with(|| {
DeviceAnalogConfig::new(device_id.to_string())
});
config.dpad_mode = mode;
info!(
"D-pad mode updated: device={}, mode={:?}",
device_id, mode
);
}
pub async fn get_dpad_mode(&self, device_id: &str) -> DpadMode {
let devices = self.devices.read().await;
if let Some(config) = devices.get(device_id) {
config.dpad_mode
} else {
DpadMode::Disabled
}
}
pub async fn load_config(
&self,
device_id: &str,
config: &crate::config::AnalogDeviceConfig,
) -> Result<(), String> {
let mut device_config = self.get_or_create_device_config(device_id).await;
device_config.inner_deadzone_x = percentage_to_raw(config.deadzone_percentage_x);
device_config.inner_deadzone_y = percentage_to_raw(config.deadzone_percentage_y);
device_config.outer_deadzone_x = percentage_to_raw(config.outer_deadzone_percentage_x);
device_config.outer_deadzone_y = percentage_to_raw(config.outer_deadzone_percentage_y);
device_config.sensitivity = config.sensitivity;
device_config.response_curve = parse_response_curve(&config.response_curve)?;
device_config.dpad_mode = parse_dpad_mode(&config.dpad_mode)?;
let mut devices = self.devices.write().await;
devices.insert(device_id.to_string(), device_config);
info!(
"Loaded analog config for device {}: deadzone_xy={}%,{}%, outer_deadzone_xy={}%,{}%, sensitivity={:.2}, curve={}, dpad={}",
device_id,
config.deadzone_percentage_x,
config.deadzone_percentage_y,
config.outer_deadzone_percentage_x,
config.outer_deadzone_percentage_y,
config.sensitivity,
config.response_curve,
config.dpad_mode
);
Ok(())
}
pub async fn save_config(
&self,
device_id: &str,
) -> Result<crate::config::AnalogDeviceConfig, String> {
let devices = self.devices.read().await;
let config = devices
.get(device_id)
.ok_or_else(|| format!("Device {} not found", device_id))?;
Ok(crate::config::AnalogDeviceConfig {
deadzone_percentage: raw_to_percentage(config.inner_deadzone_x),
deadzone_percentage_x: raw_to_percentage(config.inner_deadzone_x),
deadzone_percentage_y: raw_to_percentage(config.inner_deadzone_y),
outer_deadzone_percentage: raw_to_percentage(config.outer_deadzone_x),
outer_deadzone_percentage_x: raw_to_percentage(config.outer_deadzone_x),
outer_deadzone_percentage_y: raw_to_percentage(config.outer_deadzone_y),
sensitivity: config.sensitivity,
response_curve: response_curve_to_string(config.response_curve),
dpad_mode: dpad_mode_to_string(config.dpad_mode),
})
}
pub fn process(&self, calibration: &AnalogCalibration, x: i32, y: i32) -> (i32, i32) {
let (nx, ny) = self.normalize(x, y);
let (cx, cy) = self.center(nx, ny);
let (dx, dy) = self.apply_deadzone(cx, cy, calibration);
let (sx, sy) = self.apply_sensitivity(dx, dy, calibration);
let (ox, oy) = self.scale_to_output(sx, sy, calibration);
(ox, oy)
}
fn normalize(&self, x: i32, y: i32) -> (f32, f32) {
const INPUT_MAX: f32 = 255.0;
(
x.clamp(0, 255) as f32 / INPUT_MAX,
y.clamp(0, 255) as f32 / INPUT_MAX,
)
}
fn center(&self, x: f32, y: f32) -> (f32, f32) {
(x - 0.5, -(y - 0.5))
}
pub fn detect_dpad_direction(&self, x: f32, y: f32) -> DpadDirection {
const DIRECTION_THRESHOLD: f32 = 0.1;
let magnitude = (x * x + y * y).sqrt();
if magnitude < DIRECTION_THRESHOLD {
return DpadDirection::None;
}
let angle_rad = y.atan2(x);
let angle_deg = angle_rad.to_degrees();
let normalized_angle = if angle_deg < 0.0 {
angle_deg + 360.0
} else {
angle_deg
};
match normalized_angle {
a if a >= 337.5 || a < 22.5 => DpadDirection::Right,
a if a >= 22.5 && a < 67.5 => DpadDirection::UpRight,
a if a >= 67.5 && a < 112.5 => DpadDirection::Up,
a if a >= 112.5 && a < 157.5 => DpadDirection::UpLeft,
a if a >= 157.5 && a < 202.5 => DpadDirection::Left,
a if a >= 202.5 && a < 247.5 => DpadDirection::DownLeft,
a if a >= 247.5 && a < 292.5 => DpadDirection::Down,
a if a >= 292.5 && a < 337.5 => DpadDirection::DownRight,
_ => DpadDirection::None,
}
}
pub fn process_as_dpad(&self, calibration: &AnalogCalibration, x: i32, y: i32) -> Vec<(Key, bool)> {
let (nx, ny) = self.normalize(x, y);
let (cx, cy) = self.center(nx, ny);
let (dx, dy) = self.apply_deadzone(cx, cy, calibration);
let (ix, iy) = (
if calibration.invert_x { -dx } else { dx },
if calibration.invert_y { -dy } else { dy },
);
let (sx, sy) = (ix * 2.0, iy * 2.0);
let direction = self.detect_dpad_direction(sx, sy);
dpad_direction_to_keys(direction)
.into_iter()
.map(|k| (k, true))
.collect()
}
pub fn process_as_wasd(
&self,
calibration: &AnalogCalibration,
x: i32,
y: i32,
) -> Vec<(Key, bool)> {
let (nx, ny) = self.normalize(x, y);
let (cx, cy) = self.center(nx, ny);
let (dx, dy) = self.apply_deadzone(cx, cy, calibration);
let (ix, iy) = (
if calibration.invert_x { -dx } else { dx },
if calibration.invert_y { -dy } else { dy },
);
let (sx, sy) = (ix * 2.0, iy * 2.0);
let direction = self.detect_dpad_direction(sx, sy);
wasd_direction_to_keys(direction)
.into_iter()
.map(|k| (k, true))
.collect()
}
pub fn process_as_mouse(
&self,
calibration: &AnalogCalibration,
x: i32,
y: i32,
config: &MouseVelocityConfig,
) -> Option<(i32, i32)> {
let (processed_x, processed_y) = Self::process_2d(x, y, calibration)?;
let vel_x = ((processed_x as f32 / 32768.0) * config.multiplier) as i32;
let vel_y = ((processed_y as f32 / 32768.0) * config.multiplier) as i32;
Some((vel_x, vel_y))
}
pub fn process_as_camera(
&self,
calibration: &AnalogCalibration,
x: i32,
y: i32,
mode: CameraOutputMode,
) -> Option<CameraOutput> {
let (processed_x, processed_y) = Self::process_2d(x, y, calibration)?;
match mode {
CameraOutputMode::Scroll => {
let scroll_amount = ((processed_y as f32 / 32768.0)
* calibration.sensitivity_multiplier
* 3.0) as i32;
Some(CameraOutput::Scroll(scroll_amount))
}
CameraOutputMode::Keys => {
let sx = processed_x as f32 / 32768.0 * 2.0;
let sy = processed_y as f32 / 32768.0 * 2.0;
let direction = self.detect_dpad_direction(sx, sy);
Some(CameraOutput::Keys(camera_direction_to_keys(direction)))
}
}
}
pub async fn process_as_gamepad(
&self,
device_id: &str,
raw_x: i32,
raw_y: i32,
) -> Option<(i32, i32)> {
let devices = self.devices.read().await;
let config = devices.get(device_id)?;
let deadzone_normalized = (config.inner_deadzone_x as f32 / MAX_ABS_VALUE as f32) * 0.5;
let calibration = AnalogCalibration {
deadzone: deadzone_normalized,
deadzone_shape: DeadzoneShape::Circular,
sensitivity: SensitivityCurve::Linear,
sensitivity_multiplier: config.sensitivity,
range_min: -32768,
range_max: 32767,
invert_x: false,
invert_y: true, };
let (x, y) = Self::process_2d(raw_x, raw_y, &calibration)?;
Some((x, y))
}
pub async fn process_as_gamepad_with_calibration(
&self,
raw_x: i32,
raw_y: i32,
calibration: &AnalogCalibration,
) -> Option<(i32, i32)> {
Self::process_2d(raw_x, raw_y, calibration)
}
fn process_2d(raw_x: i32, raw_y: i32, calibration: &AnalogCalibration) -> Option<(i32, i32)> {
let nx = raw_x as f32 / 255.0;
let ny = raw_y as f32 / 255.0;
let cx = nx - 0.5;
let cy = 0.5 - ny;
let (dx, dy) = Self::apply_deadzone_static(cx, cy, calibration);
let magnitude_before = (cx * cx + cy * cy).sqrt();
if magnitude_before < calibration.deadzone {
return None;
}
let (sx, sy) = Self::apply_sensitivity_static(dx, dy, calibration);
let result = Self::scale_to_output_static(sx, sy, calibration);
Some(result)
}
fn apply_deadzone_static(x: f32, y: f32, calibration: &AnalogCalibration) -> (f32, f32) {
const MAX_MAGNITUDE: f32 = 0.70710678; const MAX_AXIS: f32 = 0.5;
match calibration.deadzone_shape {
DeadzoneShape::Circular => {
let magnitude = (x * x + y * y).sqrt();
if magnitude < calibration.deadzone {
return (0.0, 0.0);
}
let scale = if magnitude < MAX_MAGNITUDE && calibration.deadzone < MAX_MAGNITUDE {
(magnitude - calibration.deadzone) / (MAX_MAGNITUDE - calibration.deadzone)
} else {
1.0
};
(x * scale, y * scale)
}
DeadzoneShape::Square => {
let max_val = x.abs().max(y.abs());
if max_val < calibration.deadzone {
return (0.0, 0.0);
}
let dx = if x.abs() < calibration.deadzone {
0.0
} else {
let scale = (x.abs() - calibration.deadzone) / (MAX_AXIS - calibration.deadzone);
x.signum() * scale.min(1.0)
};
let dy = if y.abs() < calibration.deadzone {
0.0
} else {
let scale = (y.abs() - calibration.deadzone) / (MAX_AXIS - calibration.deadzone);
y.signum() * scale.min(1.0)
};
(dx, dy)
}
}
}
fn apply_sensitivity_static(x: f32, y: f32, calibration: &AnalogCalibration) -> (f32, f32) {
let magnitude = (x * x + y * y).sqrt();
if magnitude == 0.0 {
return (0.0, 0.0);
}
let angle = y.atan2(x);
let scaled = match calibration.sensitivity {
SensitivityCurve::Linear => magnitude,
SensitivityCurve::Quadratic => magnitude * magnitude,
SensitivityCurve::Exponential { exponent } => {
magnitude.powf(exponent.clamp(0.1, 10.0))
}
};
let result = (scaled * calibration.sensitivity_multiplier).min(1.0);
(angle.cos() * result, angle.sin() * result)
}
fn scale_to_output_static(x: f32, y: f32, calibration: &AnalogCalibration) -> (i32, i32) {
let range = (calibration.range_max - calibration.range_min) as f32;
let center = (calibration.range_min + calibration.range_max) / 2;
let mut ox = (x * range) as i32 + center;
let mut oy = (y * range) as i32 + center;
if calibration.invert_x {
ox = calibration.range_max + calibration.range_min - ox;
}
if calibration.invert_y {
oy = calibration.range_max + calibration.range_min - oy;
}
ox = ox.clamp(calibration.range_min, calibration.range_max);
oy = oy.clamp(calibration.range_min, calibration.range_max);
(ox, oy)
}
fn apply_deadzone(&self, x: f32, y: f32, calibration: &AnalogCalibration) -> (f32, f32) {
const MAX_MAGNITUDE: f32 = 0.70710678;
match calibration.deadzone_shape {
DeadzoneShape::Circular => {
let magnitude = (x * x + y * y).sqrt();
if magnitude < calibration.deadzone {
return (0.0, 0.0);
}
let scale = if magnitude < MAX_MAGNITUDE && calibration.deadzone < MAX_MAGNITUDE {
(magnitude - calibration.deadzone) / (MAX_MAGNITUDE - calibration.deadzone)
} else {
1.0
};
(x * scale, y * scale)
}
DeadzoneShape::Square => {
const MAX_AXIS: f32 = 0.5;
let max_val = x.abs().max(y.abs());
if max_val < calibration.deadzone {
return (0.0, 0.0);
}
let dx = if x.abs() < calibration.deadzone {
0.0
} else {
let scale = (x.abs() - calibration.deadzone) / (MAX_AXIS - calibration.deadzone);
x.signum() * scale.min(1.0)
};
let dy = if y.abs() < calibration.deadzone {
0.0
} else {
let scale = (y.abs() - calibration.deadzone) / (MAX_AXIS - calibration.deadzone);
y.signum() * scale.min(1.0)
};
(dx, dy)
}
}
}
fn apply_sensitivity(&self, x: f32, y: f32, calibration: &AnalogCalibration) -> (f32, f32) {
let magnitude = (x * x + y * y).sqrt();
if magnitude == 0.0 {
return (0.0, 0.0);
}
let angle = y.atan2(x);
let scaled = match calibration.sensitivity {
SensitivityCurve::Linear => magnitude,
SensitivityCurve::Quadratic => magnitude * magnitude,
SensitivityCurve::Exponential { exponent } => {
let exp = match calibration.sensitivity {
SensitivityCurve::Exponential { exponent } => exponent,
_ => 2.0,
};
magnitude.powf(exp)
}
};
let result = (scaled * calibration.sensitivity_multiplier).min(1.0);
(angle.cos() * result, angle.sin() * result)
}
fn scale_to_output(&self, x: f32, y: f32, calibration: &AnalogCalibration) -> (i32, i32) {
let range = (calibration.range_max - calibration.range_min) as f32;
let center = (calibration.range_min + calibration.range_max) / 2;
let mut ox = (x * range) as i32 + center;
let mut oy = (y * range) as i32 + center;
if calibration.invert_x {
ox = calibration.range_max + calibration.range_min - ox;
}
if calibration.invert_y {
oy = calibration.range_max + calibration.range_min - oy;
}
ox = ox.clamp(calibration.range_min, calibration.range_max);
oy = oy.clamp(calibration.range_min, calibration.range_max);
(ox, oy)
}
pub fn map_analog_to_dpad(x: f32, y: f32, mode: DpadMode) -> Vec<Direction> {
const THRESHOLD: f32 = 0.3;
let mut directions = Vec::new();
let has_up = y < -THRESHOLD;
let has_down = y > THRESHOLD;
let has_left = x < -THRESHOLD;
let has_right = x > THRESHOLD;
match mode {
DpadMode::EightWay => {
if has_up {
directions.push(Direction::Up);
}
if has_down {
directions.push(Direction::Down);
}
if has_left {
directions.push(Direction::Left);
}
if has_right {
directions.push(Direction::Right);
}
}
DpadMode::FourWay => {
let abs_x = x.abs();
let abs_y = y.abs();
if abs_x > abs_y && abs_x > THRESHOLD {
if has_left {
directions.push(Direction::Left);
} else if has_right {
directions.push(Direction::Right);
}
} else if abs_y > THRESHOLD {
if has_up {
directions.push(Direction::Up);
} else if has_down {
directions.push(Direction::Down);
}
}
}
DpadMode::Disabled => {
}
}
directions
}
}
fn percentage_to_raw(percentage: u8) -> u16 {
(percentage as u32 * MAX_ABS_VALUE as u32 / 100) as u16
}
fn raw_to_percentage(raw: u16) -> u8 {
(raw as u32 * 100 / MAX_ABS_VALUE as u32) as u8
}
fn parse_response_curve(s: &str) -> Result<ResponseCurve, String> {
match s.to_lowercase().as_str() {
"linear" => Ok(ResponseCurve::Linear),
s if s.starts_with("exponential") => {
if s == "exponential" {
Ok(ResponseCurve::Exponential { exponent: 2.0 })
} else if s.starts_with("exponential(") && s.ends_with(')') {
let inner = &s[12..s.len() - 1]; let exponent: f32 = inner
.parse()
.map_err(|_| format!("Invalid exponent: {}", inner))?;
Ok(ResponseCurve::Exponential { exponent })
} else {
Err(format!("Invalid exponential format: {}", s))
}
}
_ => Err(format!("Invalid response curve: {}", s)),
}
}
fn response_curve_to_string(curve: ResponseCurve) -> String {
match curve {
ResponseCurve::Linear => "linear".to_string(),
ResponseCurve::Exponential { exponent } => {
if (exponent - 2.0).abs() < 0.01 {
"exponential".to_string()
} else {
format!("exponential({})", exponent)
}
}
}
}
fn parse_dpad_mode(s: &str) -> Result<DpadMode, String> {
match s.to_lowercase().as_str() {
"disabled" => Ok(DpadMode::Disabled),
"eight_way" => Ok(DpadMode::EightWay),
"four_way" => Ok(DpadMode::FourWay),
_ => Err(format!("Invalid D-pad mode: {}", s)),
}
}
fn dpad_mode_to_string(mode: DpadMode) -> String {
match mode {
DpadMode::Disabled => "disabled".to_string(),
DpadMode::EightWay => "eight_way".to_string(),
DpadMode::FourWay => "four_way".to_string(),
}
}
impl Default for AnalogProcessor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_processor() -> AnalogProcessor {
AnalogProcessor::new()
}
#[tokio::test]
async fn test_analog_processor_creation() {
let processor = create_test_processor();
let devices = processor.get_configured_devices().await;
assert!(devices.is_empty(), "New processor should have no devices");
}
#[tokio::test]
async fn test_deadzone_filtering() {
let processor = create_test_processor();
let device_id = "test_device";
let result = processor.process_event(device_id, 61000, 10000).await;
assert!(result.is_none(), "Value within deadzone should be filtered");
let result = processor.process_event(device_id, 61000, -10000).await;
assert!(result.is_none(), "Negative value within deadzone should be filtered");
let result = processor.process_event(device_id, 61000, 0).await;
assert!(result.is_none(), "Center value should be filtered");
}
#[tokio::test]
async fn test_deadzone_passthrough() {
let processor = create_test_processor();
let device_id = "test_device";
let result = processor.process_event(device_id, 61000, 25000).await;
assert!(result.is_some(), "Value outside deadzone should pass through");
let result = processor.process_event(device_id, 61000, -25000).await;
assert!(result.is_some(), "Negative value outside deadzone should pass through");
let result = processor.process_event(device_id, 61000, 32767).await;
assert!(result.is_some(), "Max value should pass through");
let result = processor.process_event(device_id, 61000, -32768).await;
assert!(result.is_some(), "Min value should pass through");
}
#[tokio::test]
async fn test_sensitivity_multiplier() {
let processor = create_test_processor();
let device_id = "test_device";
processor.set_sensitivity(device_id, 2.0).await;
let output_default = create_test_processor()
.process_event(device_id, 61000, 25000)
.await;
let output_boosted = processor.process_event(device_id, 61000, 25000).await;
assert!(output_default.is_some());
assert!(output_boosted.is_some());
let default_val = output_default.unwrap();
let boosted_val = output_boosted.unwrap();
assert!(
boosted_val.abs() > default_val.abs(),
"Sensitivity 2.0 should produce higher output"
);
}
#[tokio::test]
async fn test_linear_response_curve() {
let processor = create_test_processor();
let device_id = "test_device";
let config = processor.get_or_create_device_config(device_id).await;
assert!(matches!(config.response_curve, ResponseCurve::Linear));
let result = processor.process_event(device_id, 61000, 25000).await;
assert!(result.is_some());
processor
.set_response_curve(device_id, ResponseCurve::Linear)
.await;
let result2 = processor.process_event(device_id, 61000, 25000).await;
assert!(result2.is_some());
let val1 = result.unwrap();
let val2 = result2.unwrap();
assert_eq!(val1, val2, "Linear curve should produce same output");
}
#[tokio::test]
async fn test_exponential_response_curve() {
let processor = create_test_processor();
let device_id = "test_device";
processor
.set_response_curve(device_id, ResponseCurve::Exponential { exponent: 2.0 })
.await;
let result = processor.process_event(device_id, 61000, 25000).await;
assert!(result.is_some());
let linear_result = create_test_processor()
.process_event(device_id, 61000, 25000)
.await;
let exp_result = result.unwrap();
let linear_val = linear_result.unwrap();
assert!(
exp_result.abs() < linear_val.abs(),
"Exponential curve should reduce medium values"
);
}
#[tokio::test]
async fn test_per_device_config() {
let processor = create_test_processor();
let device1 = "device1";
let device2 = "device2";
processor.set_deadzone(device1, 10000).await;
processor.set_deadzone(device2, 20000).await;
let result1 = processor.process_event(device1, 61000, 15000).await;
let result2 = processor.process_event(device2, 61000, 15000).await;
assert!(result1.is_some(), "Device1 should pass through value 15000");
assert!(result2.is_none(), "Device2 should filter value 15000");
}
#[tokio::test]
async fn test_default_config() {
let processor = create_test_processor();
let device_id = "new_device";
let config = processor.get_or_create_device_config(device_id).await;
assert_eq!(config.deadzone, DEFAULT_DEADZONE);
assert_eq!(config.sensitivity, 1.0);
assert_eq!(config.response_curve, ResponseCurve::Linear);
let config2 = processor.get_or_create_device_config(device_id).await;
assert_eq!(config.device_id, config2.device_id);
}
#[tokio::test]
async fn test_remove_device_config() {
let processor = create_test_processor();
let device_id = "test_device";
processor.set_deadzone(device_id, 15000).await;
let config = processor.get_device_config(device_id).await;
assert!(config.is_some());
processor.remove_device_config(device_id).await;
let config = processor.get_device_config(device_id).await;
assert!(config.is_none());
}
#[tokio::test]
async fn test_get_configured_devices() {
let processor = create_test_processor();
let devices = processor.get_configured_devices().await;
assert!(devices.is_empty());
processor.set_deadzone("device1", 10000).await;
processor.set_deadzone("device2", 15000).await;
processor.set_deadzone("device3", 20000).await;
let devices = processor.get_configured_devices().await;
assert_eq!(devices.len(), 3);
processor.remove_device_config("device2").await;
let devices = processor.get_configured_devices().await;
assert_eq!(devices.len(), 2);
}
#[tokio::test]
async fn test_exponential_clamping() {
let processor = create_test_processor();
let device_id = "test_device";
processor
.set_response_curve(device_id, ResponseCurve::Exponential { exponent: 10.0 })
.await;
let result = processor.process_event(device_id, 61000, 25000).await;
assert!(result.is_some(), "Exponential with high exponent should work");
processor
.set_response_curve(device_id, ResponseCurve::Exponential { exponent: 0.01 })
.await;
let result = processor.process_event(device_id, 61000, 25000).await;
assert!(result.is_some(), "Exponential with low exponent should work");
}
#[tokio::test]
async fn test_sensitivity_clamping() {
let processor = create_test_processor();
let device_id = "test_device";
processor.set_sensitivity(device_id, 10.0).await;
let config = processor.get_device_config(device_id).await;
assert!(config.is_some());
assert_eq!(config.unwrap().sensitivity, 5.0);
processor.set_sensitivity(device_id, 0.01).await;
let config = processor.get_device_config(device_id).await;
assert!(config.is_some());
assert_eq!(config.unwrap().sensitivity, 0.1);
}
#[tokio::test]
async fn test_axis_codes() {
let processor = create_test_processor();
let device_id = "test_device";
let axis_codes = [61000, 61001, 61002, 61003, 61004, 61005];
for axis in axis_codes {
let result = processor.process_event(device_id, axis, 25000).await;
assert!(
result.is_some(),
"Axis code {} should be supported",
axis
);
}
}
#[tokio::test]
async fn test_dpad_mode_disabled_by_default() {
let processor = create_test_processor();
let device_id = "test_device";
let mode = processor.get_dpad_mode(device_id).await;
assert_eq!(mode, DpadMode::Disabled);
let config = processor.get_or_create_device_config(device_id).await;
assert_eq!(config.dpad_mode, DpadMode::Disabled);
}
#[tokio::test]
async fn test_set_dpad_mode() {
let processor = create_test_processor();
let device_id = "test_device";
processor.set_dpad_mode(device_id, DpadMode::EightWay).await;
let mode = processor.get_dpad_mode(device_id).await;
assert_eq!(mode, DpadMode::EightWay);
processor.set_dpad_mode(device_id, DpadMode::FourWay).await;
let mode = processor.get_dpad_mode(device_id).await;
assert_eq!(mode, DpadMode::FourWay);
processor.set_dpad_mode(device_id, DpadMode::Disabled).await;
let mode = processor.get_dpad_mode(device_id).await;
assert_eq!(mode, DpadMode::Disabled);
}
#[tokio::test]
async fn test_dpad_eight_way_cardinals() {
let dirs = AnalogProcessor::map_analog_to_dpad(0.0, -1.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Up);
let dirs = AnalogProcessor::map_analog_to_dpad(0.0, 1.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Down);
let dirs = AnalogProcessor::map_analog_to_dpad(-1.0, 0.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Left);
let dirs = AnalogProcessor::map_analog_to_dpad(1.0, 0.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Right);
}
#[tokio::test]
async fn test_dpad_eight_way_diagonals() {
let dirs = AnalogProcessor::map_analog_to_dpad(1.0, -1.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 2);
assert!(dirs.contains(&Direction::Up));
assert!(dirs.contains(&Direction::Right));
let dirs = AnalogProcessor::map_analog_to_dpad(1.0, 1.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 2);
assert!(dirs.contains(&Direction::Down));
assert!(dirs.contains(&Direction::Right));
let dirs = AnalogProcessor::map_analog_to_dpad(-1.0, 1.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 2);
assert!(dirs.contains(&Direction::Down));
assert!(dirs.contains(&Direction::Left));
let dirs = AnalogProcessor::map_analog_to_dpad(-1.0, -1.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 2);
assert!(dirs.contains(&Direction::Up));
assert!(dirs.contains(&Direction::Left));
}
#[tokio::test]
async fn test_dpad_four_way_only_cardinals() {
let dirs = AnalogProcessor::map_analog_to_dpad(0.0, -1.0, DpadMode::FourWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Up);
let dirs = AnalogProcessor::map_analog_to_dpad(0.0, 1.0, DpadMode::FourWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Down);
let dirs = AnalogProcessor::map_analog_to_dpad(-1.0, 0.0, DpadMode::FourWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Left);
let dirs = AnalogProcessor::map_analog_to_dpad(1.0, 0.0, DpadMode::FourWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Right);
}
#[tokio::test]
async fn test_dpad_four_way_diagonals_pick_dominant() {
let dirs = AnalogProcessor::map_analog_to_dpad(1.0, -1.0, DpadMode::FourWay);
assert_eq!(dirs.len(), 1);
let dirs = AnalogProcessor::map_analog_to_dpad(0.5, -1.0, DpadMode::FourWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Up);
let dirs = AnalogProcessor::map_analog_to_dpad(1.0, -0.5, DpadMode::FourWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Right);
}
#[tokio::test]
async fn test_dpad_centered_returns_empty() {
let dirs = AnalogProcessor::map_analog_to_dpad(0.0, 0.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 0);
let dirs = AnalogProcessor::map_analog_to_dpad(0.0, 0.0, DpadMode::FourWay);
assert_eq!(dirs.len(), 0);
}
#[tokio::test]
async fn test_dpad_threshold_filters_small_movements() {
let dirs = AnalogProcessor::map_analog_to_dpad(0.2, 0.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 0);
let dirs = AnalogProcessor::map_analog_to_dpad(0.0, 0.2, DpadMode::EightWay);
assert_eq!(dirs.len(), 0);
let dirs = AnalogProcessor::map_analog_to_dpad(0.3, 0.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 0);
let dirs = AnalogProcessor::map_analog_to_dpad(0.31, 0.0, DpadMode::EightWay);
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0], Direction::Right);
}
#[tokio::test]
async fn test_config_save_load_roundtrip() {
let processor = create_test_processor();
let device_id = "test_device";
processor.set_deadzone_percentage_x(device_id, 30).await.unwrap();
processor.set_deadzone_percentage_y(device_id, 60).await.unwrap();
processor.set_sensitivity(device_id, 2.0).await;
processor.set_response_curve(device_id, ResponseCurve::Exponential { exponent: 3.0 }).await;
processor.set_dpad_mode(device_id, DpadMode::EightWay).await;
let saved = processor.save_config(device_id).await.unwrap();
assert!((saved.deadzone_percentage_x as i32 - 30i32).abs() <= 1);
assert!((saved.deadzone_percentage_y as i32 - 60i32).abs() <= 1);
assert_eq!(saved.sensitivity, 2.0);
assert_eq!(saved.response_curve, "exponential(3)");
assert_eq!(saved.dpad_mode, "eight_way");
let processor2 = AnalogProcessor::new();
processor2.load_config(device_id, &saved).await.unwrap();
let loaded_config = processor2.get_device_config(device_id).await.unwrap();
assert!((raw_to_percentage(loaded_config.inner_deadzone_x) as i32 - 30i32).abs() <= 2);
assert!((raw_to_percentage(loaded_config.inner_deadzone_y) as i32 - 60i32).abs() <= 2);
assert_eq!(loaded_config.sensitivity, 2.0);
assert_eq!(loaded_config.response_curve, ResponseCurve::Exponential { exponent: 3.0 });
assert_eq!(loaded_config.dpad_mode, DpadMode::EightWay);
}
#[tokio::test]
async fn test_config_default_values() {
let processor = create_test_processor();
let device_id = "test_device";
let minimal_config = crate::config::AnalogDeviceConfig {
deadzone_percentage: 43,
deadzone_percentage_x: 43,
deadzone_percentage_y: 43,
outer_deadzone_percentage: 100,
outer_deadzone_percentage_x: 100,
outer_deadzone_percentage_y: 100,
sensitivity: 1.5,
response_curve: "linear".to_string(),
dpad_mode: "disabled".to_string(),
};
processor.load_config(device_id, &minimal_config).await.unwrap();
let config = processor.get_device_config(device_id).await.unwrap();
assert_eq!(config.sensitivity, 1.5);
assert_eq!(config.response_curve, ResponseCurve::Linear);
assert_eq!(config.dpad_mode, DpadMode::Disabled);
}
#[tokio::test]
async fn test_config_percentage_conversion() {
let processor = create_test_processor();
let device_id = "test_device";
for percentage in [0u8, 20, 43, 50, 80, 100].iter() {
let raw = percentage_to_raw(*percentage);
let converted = raw_to_percentage(raw);
assert!(
(converted as i32 - *percentage as i32).abs() <= 1,
"Percentage conversion failed: {}% -> {} raw -> {}%",
percentage, raw, converted
);
}
}
#[tokio::test]
async fn test_load_applies_all_settings() {
let processor = create_test_processor();
let device_id = "test_device";
let config = crate::config::AnalogDeviceConfig {
deadzone_percentage: 25,
deadzone_percentage_x: 30,
deadzone_percentage_y: 70,
outer_deadzone_percentage: 95,
outer_deadzone_percentage_x: 90,
outer_deadzone_percentage_y: 95,
sensitivity: 1.8,
response_curve: "exponential(2.5)".to_string(),
dpad_mode: "four_way".to_string(),
};
processor.load_config(device_id, &config).await.unwrap();
let device_config = processor.get_device_config(device_id).await.unwrap();
assert!((raw_to_percentage(device_config.inner_deadzone_x) as i32 - 30i32).abs() <= 1);
assert!((raw_to_percentage(device_config.inner_deadzone_y) as i32 - 70i32).abs() <= 1);
assert!((raw_to_percentage(device_config.outer_deadzone_x) as i32 - 90i32).abs() <= 1);
assert!((raw_to_percentage(device_config.outer_deadzone_y) as i32 - 95i32).abs() <= 1);
assert_eq!(device_config.sensitivity, 1.8);
assert_eq!(device_config.response_curve, ResponseCurve::Exponential { exponent: 2.5 });
assert_eq!(device_config.dpad_mode, DpadMode::FourWay);
}
#[tokio::test]
async fn test_save_serializes_all_settings() {
let processor = create_test_processor();
let device_id = "test_device";
processor.set_deadzone_percentage_x(device_id, 40).await.unwrap();
processor.set_deadzone_percentage_y(device_id, 75).await.unwrap();
processor.set_outer_deadzone_x(device_id, (90f32 * 32767.0 / 100.0) as u16).await;
processor.set_outer_deadzone_y(device_id, (85f32 * 32767.0 / 100.0) as u16).await;
processor.set_sensitivity(device_id, 2.2).await;
processor.set_response_curve(device_id, ResponseCurve::Exponential { exponent: 4.0 }).await;
processor.set_dpad_mode(device_id, DpadMode::EightWay).await;
let saved = processor.save_config(device_id).await.unwrap();
assert!((saved.deadzone_percentage_x as i32 - 40i32).abs() <= 1);
assert!((saved.deadzone_percentage_y as i32 - 75i32).abs() <= 1);
assert!((saved.outer_deadzone_percentage_x as i32 - 90i32).abs() <= 1);
assert!((saved.outer_deadzone_percentage_y as i32 - 85i32).abs() <= 1);
assert_eq!(saved.sensitivity, 2.2);
assert_eq!(saved.response_curve, "exponential(4)");
assert_eq!(saved.dpad_mode, "eight_way");
}
#[tokio::test]
async fn test_multiple_devices_configs() {
let processor = create_test_processor();
let device1 = "device1";
let device2 = "device2";
processor.set_deadzone_percentage_x(device1, 20).await.unwrap();
processor.set_sensitivity(device1, 0.5).await;
processor.set_deadzone_percentage_x(device2, 80).await.unwrap();
processor.set_sensitivity(device2, 3.0).await;
let config1 = processor.get_device_config(device1).await.unwrap();
let config2 = processor.get_device_config(device2).await.unwrap();
assert!((raw_to_percentage(config1.inner_deadzone_x) as i32 - 20i32).abs() <= 1);
assert_eq!(config1.sensitivity, 0.5);
assert!((raw_to_percentage(config2.inner_deadzone_x) as i32 - 80i32).abs() <= 1);
assert_eq!(config2.sensitivity, 3.0);
}
#[test]
fn test_process_center_position() {
let processor = create_test_processor();
let calibration = AnalogCalibration::default();
let (x, y) = processor.process(&calibration, 128, 128);
assert!(x.abs() < 1000, "X should be near center, got {}", x);
assert!(y.abs() < 1000, "Y should be near center, got {}", y);
}
#[test]
fn test_process_deadzone_filtering() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.2;
let (x, y) = processor.process(&calibration, 130, 130);
assert_eq!(x, 0, "Small movement should be filtered");
assert_eq!(y, 0, "Small movement should be filtered");
}
#[test]
fn test_process_circular_deadzone() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.15;
calibration.deadzone_shape = DeadzoneShape::Circular;
let (x, y) = processor.process(&calibration, 145, 145);
assert_eq!(x, 0, "Small diagonal movement should be filtered");
assert_eq!(y, 0, "Small diagonal movement should be filtered");
let (x, y) = processor.process(&calibration, 200, 200);
assert!(x != 0 || y != 0, "Large diagonal movement should not be filtered");
}
#[test]
fn test_process_square_deadzone() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.15;
calibration.deadzone_shape = DeadzoneShape::Square;
let (x, y) = processor.process(&calibration, 140, 128);
assert_eq!(x, 0, "Small X movement should be filtered");
let (x, y) = processor.process(&calibration, 180, 128);
assert!(x != 0, "Large X movement should not be filtered");
}
#[test]
fn test_process_linear_sensitivity() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let (x, y) = processor.process(&calibration, 255, 255);
assert!(x > 30000, "Full X should give large output, got {}", x);
assert!(y > 30000 || y < -30000, "Full Y should give large output, got {}", y);
}
#[test]
fn test_process_quadratic_sensitivity() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.sensitivity = SensitivityCurve::Quadratic;
let (qx, qy) = processor.process(&calibration, 200, 128);
calibration.sensitivity = SensitivityCurve::Linear;
let (lx, ly) = processor.process(&calibration, 200, 128);
assert!(qx.abs() < lx.abs(), "Quadratic should reduce medium X values");
}
#[test]
fn test_process_exponential_sensitivity() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.sensitivity = SensitivityCurve::Exponential { exponent: 3.0 };
let (ex, ey) = processor.process(&calibration, 200, 128);
calibration.sensitivity = SensitivityCurve::Linear;
let (lx, ly) = processor.process(&calibration, 200, 128);
assert!(ex.abs() < lx.abs(), "Exponential should reduce medium X values");
}
#[test]
fn test_process_range_scaling() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.range_min = -100;
calibration.range_max = 100;
let (x, y) = processor.process(&calibration, 255, 0);
assert!(x <= 100, "X should be clamped to max range");
assert!(y >= -100, "Y should be clamped to min range");
assert!(x >= -100, "X should be at least min range");
assert!(y <= 100, "Y should be at most max range");
}
#[test]
fn test_process_inversion() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
let (x1, y1) = processor.process(&calibration, 255, 128);
calibration.invert_x = true;
let (x2, y2) = processor.process(&calibration, 255, 128);
assert!((x1 + x2).abs() < 1000, "Inverted X should be opposite direction");
calibration.invert_x = false;
calibration.invert_y = true;
let (x3, y3) = processor.process(&calibration, 128, 0);
calibration.invert_y = false;
let (x4, y4) = processor.process(&calibration, 128, 0);
assert!((y3 + y4).abs() < 1000, "Inverted Y should be opposite direction");
}
#[test]
fn test_process_full_pipeline() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.1;
calibration.deadzone_shape = DeadzoneShape::Circular;
calibration.sensitivity = SensitivityCurve::Quadratic;
calibration.sensitivity_multiplier = 1.5;
calibration.range_min = -32768;
calibration.range_max = 32767;
let (x, y) = processor.process(&calibration, 230, 200);
assert!(x != 0, "Large X movement should not be filtered");
assert!(y < 0, "Y=200 should give negative output");
}
#[test]
fn test_process_normalize_clamps_input() {
let processor = create_test_processor();
let calibration = AnalogCalibration::default();
let (x, y) = processor.process(&calibration, 300, -50);
assert!(x >= calibration.range_min);
assert!(x <= calibration.range_max);
assert!(y >= calibration.range_min);
assert!(y <= calibration.range_max);
}
#[test]
fn test_process_sensitivity_multiplier() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.sensitivity_multiplier = 2.0;
let (x1, y1) = processor.process(&calibration, 200, 128);
calibration.sensitivity_multiplier = 0.5;
let (x2, y2) = processor.process(&calibration, 200, 128);
assert!(x1.abs() > x2.abs(), "Higher multiplier should give larger output");
}
#[test]
fn test_dpad_center_returns_empty() {
let processor = create_test_processor();
let calibration = AnalogCalibration::default();
let result = processor.process_as_dpad(&calibration, 128, 128);
assert!(result.is_empty(), "Center position should return no keys");
}
#[test]
fn test_dpad_cardinal_up() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 128, 0);
assert_eq!(result.len(), 1, "Up should return 1 key");
assert_eq!(result[0].0, Key::KEY_UP, "Should be KEY_UP");
assert!(result[0].1, "Key should be pressed");
}
#[test]
fn test_dpad_cardinal_down() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 128, 255);
assert_eq!(result.len(), 1, "Down should return 1 key");
assert_eq!(result[0].0, Key::KEY_DOWN, "Should be KEY_DOWN");
assert!(result[0].1, "Key should be pressed");
}
#[test]
fn test_dpad_cardinal_left() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 0, 128);
assert_eq!(result.len(), 1, "Left should return 1 key");
assert_eq!(result[0].0, Key::KEY_LEFT, "Should be KEY_LEFT");
assert!(result[0].1, "Key should be pressed");
}
#[test]
fn test_dpad_cardinal_right() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 255, 128);
assert_eq!(result.len(), 1, "Right should return 1 key");
assert_eq!(result[0].0, Key::KEY_RIGHT, "Should be KEY_RIGHT");
assert!(result[0].1, "Key should be pressed");
}
#[test]
fn test_dpad_diagonal_up_right() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 255, 0);
assert_eq!(result.len(), 2, "Up-Right should return 2 keys");
let keys: Vec<Key> = result.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&Key::KEY_UP), "Should contain KEY_UP");
assert!(keys.contains(&Key::KEY_RIGHT), "Should contain KEY_RIGHT");
assert!(result.iter().all(|(_, pressed)| *pressed), "All keys should be pressed");
}
#[test]
fn test_dpad_diagonal_down_left() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 0, 255);
assert_eq!(result.len(), 2, "Down-Left should return 2 keys");
let keys: Vec<Key> = result.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&Key::KEY_DOWN), "Should contain KEY_DOWN");
assert!(keys.contains(&Key::KEY_LEFT), "Should contain KEY_LEFT");
}
#[test]
fn test_dpad_all_eight_directions() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 255, 128);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, Key::KEY_RIGHT);
let result = processor.process_as_dpad(&calibration, 255, 200);
assert!(result.len() >= 1);
let result = processor.process_as_dpad(&calibration, 128, 255);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, Key::KEY_DOWN);
let result = processor.process_as_dpad(&calibration, 0, 200);
assert!(result.len() >= 1);
let result = processor.process_as_dpad(&calibration, 0, 128);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, Key::KEY_LEFT);
let result = processor.process_as_dpad(&calibration, 0, 50);
assert!(result.len() >= 1);
let result = processor.process_as_dpad(&calibration, 128, 0);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, Key::KEY_UP);
let result = processor.process_as_dpad(&calibration, 255, 50);
assert!(result.len() >= 1);
}
#[test]
fn test_dpad_deadzone_filters_small_movements() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.2;
let result = processor.process_as_dpad(&calibration, 135, 128);
assert!(result.is_empty(), "Small X movement should be filtered by deadzone");
let result = processor.process_as_dpad(&calibration, 128, 135);
assert!(result.is_empty(), "Small Y movement should be filtered by deadzone");
}
#[test]
fn test_dpad_direction_threshold() {
let processor = create_test_processor();
let calibration = AnalogCalibration::default();
let result = processor.process_as_dpad(&calibration, 130, 128);
assert!(result.is_empty(), "Tiny X movement should be filtered");
let result = processor.process_as_dpad(&calibration, 128, 130);
assert!(result.is_empty(), "Tiny Y movement should be filtered");
}
#[test]
fn test_dpad_inversion() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_dpad(&calibration, 255, 128);
assert_eq!(result[0].0, Key::KEY_RIGHT);
calibration.invert_x = true;
let result = processor.process_as_dpad(&calibration, 255, 128);
assert_eq!(result[0].0, Key::KEY_LEFT, "X inversion should flip direction");
calibration.invert_x = false;
calibration.invert_y = true;
let result = processor.process_as_dpad(&calibration, 128, 0);
assert_eq!(result[0].0, Key::KEY_DOWN, "Y inversion should flip direction");
}
#[test]
fn test_dpad_all_pressed_true() {
let processor = create_test_processor();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let test_cases = [
(255, 128), (0, 128), (128, 0), (128, 255), (255, 0), (0, 255), ];
for (x, y) in test_cases {
let result = processor.process_as_dpad(&calibration, x, y);
if !result.is_empty() {
assert!(
result.iter().all(|(_, pressed)| *pressed),
"All keys should be pressed=true for ({}, {})",
x, y
);
}
}
}
#[test]
fn test_dpad_direction_to_keys() {
let keys = dpad_direction_to_keys(DpadDirection::None);
assert!(keys.is_empty());
let keys = dpad_direction_to_keys(DpadDirection::Up);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_UP);
let keys = dpad_direction_to_keys(DpadDirection::Down);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_DOWN);
let keys = dpad_direction_to_keys(DpadDirection::Left);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_LEFT);
let keys = dpad_direction_to_keys(DpadDirection::Right);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_RIGHT);
let keys = dpad_direction_to_keys(DpadDirection::UpRight);
assert_eq!(keys.len(), 2);
assert!(keys.contains(&Key::KEY_UP));
assert!(keys.contains(&Key::KEY_RIGHT));
let keys = dpad_direction_to_keys(DpadDirection::DownLeft);
assert_eq!(keys.len(), 2);
assert!(keys.contains(&Key::KEY_DOWN));
assert!(keys.contains(&Key::KEY_LEFT));
}
#[tokio::test]
async fn test_process_as_gamepad_center() {
let processor = create_test_processor();
let device_id = "32b6:12f7";
let result = processor.process_as_gamepad(device_id, 127, 127).await;
assert!(result.is_none(), "Center position should be filtered by deadzone");
}
#[tokio::test]
async fn test_process_as_gamepad_full_deflection() {
let processor = create_test_processor();
let device_id = "32b6:12f7";
let _config = processor.get_or_create_device_config(device_id).await;
if let Some((x, y)) = processor.process_as_gamepad(device_id, 255, 127).await {
assert!(x > 15000, "X should be high positive, got {}", x);
assert!(y.abs() < 5000, "Y should be near center, got {}", y);
} else {
panic!("Full right deflection should produce output");
}
if let Some((x, y)) = processor.process_as_gamepad(device_id, 127, 0).await {
assert!(x.abs() < 5000, "X should be near center, got {}", x);
assert!(y < -15000, "Y should be high negative (up), got {}", y);
} else {
panic!("Full up deflection should produce output");
}
if let Some((x, y)) = processor.process_as_gamepad(device_id, 0, 127).await {
assert!(x < -15000, "X should be high negative, got {}", x);
assert!(y.abs() < 5000, "Y should be near center, got {}", y);
} else {
panic!("Full left deflection should produce output");
}
if let Some((x, y)) = processor.process_as_gamepad(device_id, 127, 255).await {
assert!(x.abs() < 5000, "X should be near center, got {}", x);
assert!(y > 15000, "Y should be high positive (down), got {}", y);
} else {
panic!("Full down deflection should produce output");
}
}
#[tokio::test]
async fn test_process_as_gamepad_diagonal() {
let processor = create_test_processor();
let device_id = "32b6:12f7";
let _config = processor.get_or_create_device_config(device_id).await;
if let Some((x, y)) = processor.process_as_gamepad(device_id, 255, 0).await {
assert!(x > 10000, "X should be positive, got {}", x);
assert!(y < -10000, "Y should be negative (up), got {}", y);
} else {
panic!("Diagonal deflection should produce output");
}
if let Some((x, y)) = processor.process_as_gamepad(device_id, 0, 255).await {
assert!(x < -10000, "X should be negative, got {}", x);
assert!(y > 10000, "Y should be positive (down), got {}", y);
} else {
panic!("Diagonal deflection should produce output");
}
}
#[tokio::test]
async fn test_process_as_gamepad_edge_positions() {
let processor = create_test_processor();
let device_id = "test_device";
let _config = processor.get_or_create_device_config(device_id).await;
let result = processor.process_as_gamepad(device_id, 200, 127).await;
assert!(result.is_some(), "Value (200, 127) should produce output");
let _result = processor.process_as_gamepad(device_id, 135, 127).await;
}
#[tokio::test]
async fn test_process_as_gamepad_unknown_device() {
let processor = create_test_processor();
let result = processor.process_as_gamepad("unknown:1234", 255, 127).await;
assert!(result.is_none(), "Unknown device should return None");
}
#[test]
fn test_process_2d_center() {
let calibration = AnalogCalibration::default();
let result = AnalogProcessor::process_2d(127, 127, &calibration);
assert!(result.is_none(), "Center position should be filtered by deadzone");
}
#[test]
fn test_process_2d_full_deflection() {
let calibration = AnalogCalibration::default();
if let Some((x, y)) = AnalogProcessor::process_2d(255, 127, &calibration) {
assert!(x > 15000, "X should be high positive, got {}", x);
assert!(y.abs() < 5000, "Y should be near center, got {}", y);
} else {
panic!("Full right deflection should produce output");
}
if let Some((x, y)) = AnalogProcessor::process_2d(127, 0, &calibration) {
assert!(x.abs() < 5000, "X should be near center, got {}", x);
assert!(y > 15000, "Y should be high positive, got {}", y);
} else {
panic!("Full up deflection should produce output");
}
}
#[test]
fn test_apply_deadzone_static_circular() {
let calibration = AnalogCalibration::with_deadzone(0.2);
let (x, y) = AnalogProcessor::apply_deadzone_static(0.1, 0.1, &calibration);
assert_eq!(x, 0.0, "Should be filtered");
assert_eq!(y, 0.0, "Should be filtered");
let (x, y) = AnalogProcessor::apply_deadzone_static(0.4, 0.0, &calibration);
assert!(x > 0.0, "Should be scaled outward");
assert_eq!(y, 0.0, "Y should remain 0");
}
#[test]
fn test_apply_deadzone_static_square() {
let calibration = AnalogCalibration {
deadzone: 0.2,
deadzone_shape: DeadzoneShape::Square,
..Default::default()
};
let (x, y) = AnalogProcessor::apply_deadzone_static(0.1, 0.3, &calibration);
assert_eq!(x, 0.0, "X should be filtered");
assert!(y > 0.0, "Y should pass through");
let (x, y) = AnalogProcessor::apply_deadzone_static(0.3, 0.3, &calibration);
assert!(x > 0.0, "X should pass through");
assert!(y > 0.0, "Y should pass through");
}
#[test]
fn test_apply_sensitivity_static_linear() {
let calibration = AnalogCalibration::default();
let (x, y) = AnalogProcessor::apply_sensitivity_static(0.5, 0.0, &calibration);
assert!(x > 0.0, "X should be positive");
assert!(y.abs() < 0.01, "Y should be near 0, got {}", y);
let (x, y) = AnalogProcessor::apply_sensitivity_static(0.0, 0.5, &calibration);
assert!(x.abs() < 0.01, "X should be near 0, got {}", x);
assert!(y > 0.0, "Y should be positive");
}
#[test]
fn test_apply_sensitivity_static_quadratic() {
let calibration = AnalogCalibration {
sensitivity: SensitivityCurve::Quadratic,
..Default::default()
};
let (x1, _) = AnalogProcessor::apply_sensitivity_static(0.3, 0.0, &AnalogCalibration::default());
let (x2, _) = AnalogProcessor::apply_sensitivity_static(0.3, 0.0, &calibration);
assert!(x2 < x1, "Quadratic should produce smaller output for same input");
}
#[test]
fn test_scale_to_output_static() {
let calibration = AnalogCalibration::default();
let (x, _y) = AnalogProcessor::scale_to_output_static(0.0, 0.0, &calibration);
assert!(x.abs() < 100, "X should be near 0");
let (x, _y) = AnalogProcessor::scale_to_output_static(0.5, 0.0, &calibration);
assert!(x > 30000, "X should be near max");
let (x, _y) = AnalogProcessor::scale_to_output_static(-0.5, 0.0, &calibration);
assert!(x < -30000, "X should be near min");
}
#[test]
fn test_scale_to_output_static_inversion() {
let calibration = AnalogCalibration {
invert_x: true,
invert_y: true,
..Default::default()
};
let (x, _y) = AnalogProcessor::scale_to_output_static(0.5, 0.0, &calibration);
assert!(x < -30000, "Inverted X should be near min");
let (_x, y) = AnalogProcessor::scale_to_output_static(0.0, 0.5, &calibration);
assert!(y < -30000, "Inverted Y should be near min");
}
#[tokio::test]
async fn test_process_as_gamepad_deadzone_filters_center() {
let processor = AnalogProcessor::new();
let device_id = "32b6:12f7";
{
let mut devices = processor.devices.write().await;
devices.insert(device_id.to_string(), DeviceAnalogConfig::new(device_id.to_string()));
}
let result = processor.process_as_gamepad(device_id, 127, 127).await;
assert!(result.is_none(), "Center should be filtered by deadzone");
}
#[tokio::test]
async fn test_process_as_gamepad_full_right() {
let processor = AnalogProcessor::new();
let device_id = "32b6:12f7";
{
let mut devices = processor.devices.write().await;
devices.insert(device_id.to_string(), DeviceAnalogConfig::new(device_id.to_string()));
}
let result = processor.process_as_gamepad(device_id, 255, 127).await;
assert!(result.is_some(), "Full right should produce output");
let (x, y) = result.unwrap();
assert!(x > 15000, "X should be high positive, got {}", x);
assert!(y.abs() < 5000, "Y should be near center, got {}", y);
}
#[tokio::test]
async fn test_process_as_gamepad_full_left() {
let processor = AnalogProcessor::new();
let device_id = "32b6:12f7";
{
let mut devices = processor.devices.write().await;
devices.insert(device_id.to_string(), DeviceAnalogConfig::new(device_id.to_string()));
}
let result = processor.process_as_gamepad(device_id, 0, 127).await;
assert!(result.is_some());
let (x, y) = result.unwrap();
assert!(x < -15000, "X should be high negative, got {}", x);
assert!(y.abs() < 5000, "Y should be near center, got {}", y);
}
#[tokio::test]
async fn test_process_as_gamepad_full_up() {
let processor = AnalogProcessor::new();
let device_id = "32b6:12f7";
{
let mut devices = processor.devices.write().await;
devices.insert(device_id.to_string(), DeviceAnalogConfig::new(device_id.to_string()));
}
let result = processor.process_as_gamepad(device_id, 127, 0).await;
assert!(result.is_some());
let (x, y) = result.unwrap();
assert!(x.abs() < 5000, "X should be near center, got {}", x);
assert!(y < -15000, "Y should be high negative (up), got {}", y);
}
#[tokio::test]
async fn test_process_as_gamepad_full_down() {
let processor = AnalogProcessor::new();
let device_id = "32b6:12f7";
{
let mut devices = processor.devices.write().await;
devices.insert(device_id.to_string(), DeviceAnalogConfig::new(device_id.to_string()));
}
let result = processor.process_as_gamepad(device_id, 127, 255).await;
assert!(result.is_some());
let (x, y) = result.unwrap();
assert!(x.abs() < 5000, "X should be near center, got {}", x);
assert!(y > 15000, "Y should be high positive (down), got {}", y);
}
#[tokio::test]
async fn test_process_as_gamepad_diagonal_up_right() {
let processor = AnalogProcessor::new();
let device_id = "32b6:12f7";
{
let mut devices = processor.devices.write().await;
devices.insert(device_id.to_string(), DeviceAnalogConfig::new(device_id.to_string()));
}
let result = processor.process_as_gamepad(device_id, 255, 0).await;
assert!(result.is_some());
let (x, y) = result.unwrap();
assert!(x > 10000, "X should be positive, got {}", x);
assert!(y < -10000, "Y should be negative (up), got {}", y);
}
#[tokio::test]
async fn test_process_as_gamepad_sensitivity_affects_output() {
let processor = AnalogProcessor::new();
let device_id = "test_sens";
{
let mut devices = processor.devices.write().await;
let mut config = DeviceAnalogConfig::new(device_id.to_string());
config.sensitivity = 2.0; devices.insert(device_id.to_string(), config);
}
let result = processor.process_as_gamepad(device_id, 200, 127).await;
assert!(result.is_some());
let (x, _) = result.unwrap();
assert!(x > 0, "X should be positive");
}
#[tokio::test]
async fn test_process_as_gamepad_with_calibration() {
use crate::analog_calibration::{AnalogCalibration, DeadzoneShape, SensitivityCurve};
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration {
deadzone_shape: DeadzoneShape::Circular,
deadzone: 0.2, sensitivity: SensitivityCurve::Quadratic,
sensitivity_multiplier: 1.5,
range_min: -32768,
range_max: 32767,
invert_x: false,
invert_y: false,
};
let result = processor.process_as_gamepad_with_calibration(200, 127, &calibration).await;
assert!(result.is_some(), "Should produce output outside deadzone");
}
#[test]
fn test_wasd_direction_to_keys_all_directions() {
let keys = wasd_direction_to_keys(DpadDirection::None);
assert!(keys.is_empty(), "None should return empty Vec");
let keys = wasd_direction_to_keys(DpadDirection::Up);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_W, "Up should map to W");
let keys = wasd_direction_to_keys(DpadDirection::Down);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_S, "Down should map to S");
let keys = wasd_direction_to_keys(DpadDirection::Left);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_A, "Left should map to A");
let keys = wasd_direction_to_keys(DpadDirection::Right);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_D, "Right should map to D");
let keys = wasd_direction_to_keys(DpadDirection::UpRight);
assert_eq!(keys.len(), 2, "UpRight should return 2 keys");
assert!(keys.contains(&Key::KEY_W), "UpRight should contain W");
assert!(keys.contains(&Key::KEY_D), "UpRight should contain D");
let keys = wasd_direction_to_keys(DpadDirection::UpLeft);
assert_eq!(keys.len(), 2, "UpLeft should return 2 keys");
assert!(keys.contains(&Key::KEY_W), "UpLeft should contain W");
assert!(keys.contains(&Key::KEY_A), "UpLeft should contain A");
let keys = wasd_direction_to_keys(DpadDirection::DownRight);
assert_eq!(keys.len(), 2, "DownRight should return 2 keys");
assert!(keys.contains(&Key::KEY_S), "DownRight should contain S");
assert!(keys.contains(&Key::KEY_D), "DownRight should contain D");
let keys = wasd_direction_to_keys(DpadDirection::DownLeft);
assert_eq!(keys.len(), 2, "DownLeft should return 2 keys");
assert!(keys.contains(&Key::KEY_S), "DownLeft should contain S");
assert!(keys.contains(&Key::KEY_A), "DownLeft should contain A");
}
#[test]
fn test_process_as_wasd_deadzone_filters_center() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let result = processor.process_as_wasd(&calibration, 128, 128);
assert!(result.is_empty(), "Center position should return no keys");
}
#[test]
fn test_process_as_wasd_cardinal_directions() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_wasd(&calibration, 128, 0);
assert_eq!(result.len(), 1, "North should return 1 key");
assert_eq!(result[0].0, Key::KEY_W, "North should be W key");
assert!(result[0].1, "Key should be pressed");
let result = processor.process_as_wasd(&calibration, 128, 255);
assert_eq!(result.len(), 1, "South should return 1 key");
assert_eq!(result[0].0, Key::KEY_S, "South should be S key");
let result = processor.process_as_wasd(&calibration, 0, 128);
assert_eq!(result.len(), 1, "West should return 1 key");
assert_eq!(result[0].0, Key::KEY_A, "West should be A key");
let result = processor.process_as_wasd(&calibration, 255, 128);
assert_eq!(result.len(), 1, "East should return 1 key");
assert_eq!(result[0].0, Key::KEY_D, "East should be D key");
}
#[test]
fn test_process_as_wasd_diagonal_directions() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_wasd(&calibration, 255, 0);
assert_eq!(result.len(), 2, "North-East should return 2 keys");
let keys: Vec<Key> = result.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&Key::KEY_W), "North-East should contain W");
assert!(keys.contains(&Key::KEY_D), "North-East should contain D");
let result = processor.process_as_wasd(&calibration, 0, 0);
assert_eq!(result.len(), 2, "North-West should return 2 keys");
let keys: Vec<Key> = result.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&Key::KEY_W), "North-West should contain W");
assert!(keys.contains(&Key::KEY_A), "North-West should contain A");
let result = processor.process_as_wasd(&calibration, 255, 255);
assert_eq!(result.len(), 2, "South-East should return 2 keys");
let keys: Vec<Key> = result.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&Key::KEY_S), "South-East should contain S");
assert!(keys.contains(&Key::KEY_D), "South-East should contain D");
let result = processor.process_as_wasd(&calibration, 0, 255);
assert_eq!(result.len(), 2, "South-West should return 2 keys");
let keys: Vec<Key> = result.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&Key::KEY_S), "South-West should contain S");
assert!(keys.contains(&Key::KEY_A), "South-West should contain A");
}
#[test]
fn test_process_as_wasd_axis_inversion() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_wasd(&calibration, 255, 128);
assert_eq!(result[0].0, Key::KEY_D, "Normal X should be right (D)");
calibration.invert_x = true;
let result = processor.process_as_wasd(&calibration, 255, 128);
assert_eq!(result[0].0, Key::KEY_A, "Inverted X should be left (A)");
calibration.invert_x = false;
calibration.invert_y = true;
let result = processor.process_as_wasd(&calibration, 128, 0);
assert_eq!(result[0].0, Key::KEY_S, "Inverted Y should be down (S)");
calibration.invert_x = true;
calibration.invert_y = true;
let result = processor.process_as_wasd(&calibration, 255, 0);
let keys: Vec<Key> = result.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&Key::KEY_A), "Double-inverted X should be left (A)");
assert!(keys.contains(&Key::KEY_S), "Double-inverted Y should be down (S)");
}
#[test]
fn test_process_as_wasd_all_pressed_true() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let test_cases = [
(255, 128), (0, 128), (128, 0), (128, 255), (255, 0), (0, 255), ];
for (x, y) in test_cases {
let result = processor.process_as_wasd(&calibration, x, y);
if !result.is_empty() {
assert!(
result.iter().all(|(_, pressed)| *pressed),
"All keys should be pressed=true for ({}, {})",
x, y
);
}
}
}
#[test]
fn test_process_as_wasd_deadzone_filters_small_movements() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.2;
let result = processor.process_as_wasd(&calibration, 135, 128);
assert!(result.is_empty(), "Small X movement should be filtered by deadzone");
let result = processor.process_as_wasd(&calibration, 128, 135);
assert!(result.is_empty(), "Small Y movement should be filtered by deadzone");
let result = processor.process_as_wasd(&calibration, 128, 128);
assert!(result.is_empty(), "Center should be filtered by deadzone");
}
#[test]
fn test_process_as_mouse_deadzone_filters_center() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 128, 128, &config);
assert!(result.is_none(), "Center position should return None");
}
#[test]
fn test_process_as_mouse_full_right() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 255, 128, &config);
assert!(result.is_some(), "Full right should return velocity");
let (vel_x, vel_y) = result.unwrap();
assert!(vel_x > 0, "Full right should have positive X velocity, got {}", vel_x);
assert_eq!(vel_y, 0, "Full right should have zero Y velocity");
}
#[test]
fn test_process_as_mouse_full_left() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 0, 128, &config);
assert!(result.is_some(), "Full left should return velocity");
let (vel_x, vel_y) = result.unwrap();
assert!(vel_x < 0, "Full left should have negative X velocity, got {}", vel_x);
assert_eq!(vel_y, 0, "Full left should have zero Y velocity");
}
#[test]
fn test_process_as_mouse_full_up() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 128, 0, &config);
assert!(result.is_some(), "Full up should return velocity");
let (vel_x, vel_y) = result.unwrap();
assert_eq!(vel_x, 0, "Full up should have zero X velocity");
assert!(vel_y > 0, "Full up should have positive Y velocity, got {}", vel_y);
}
#[test]
fn test_process_as_mouse_full_down() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 128, 255, &config);
assert!(result.is_some(), "Full down should return velocity");
let (vel_x, vel_y) = result.unwrap();
assert_eq!(vel_x, 0, "Full down should have zero X velocity");
assert!(vel_y < 0, "Full down should have negative Y velocity, got {}", vel_y);
}
#[test]
fn test_process_as_mouse_multiplier_affects_velocity() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let config_default = MouseVelocityConfig::default();
let result1 = processor.process_as_mouse(&calibration, 255, 128, &config_default);
assert!(result1.is_some());
let (vel_x1, _) = result1.unwrap();
let config_high = MouseVelocityConfig { multiplier: 20.0 };
let result2 = processor.process_as_mouse(&calibration, 255, 128, &config_high);
assert!(result2.is_some());
let (vel_x2, _) = result2.unwrap();
assert!(vel_x2 > vel_x1, "Higher multiplier should give higher velocity: {} > {}", vel_x2, vel_x1);
let config_low = MouseVelocityConfig { multiplier: 5.0 };
let result3 = processor.process_as_mouse(&calibration, 255, 128, &config_low);
assert!(result3.is_some());
let (vel_x3, _) = result3.unwrap();
assert!(vel_x3 < vel_x1, "Lower multiplier should give lower velocity: {} < {}", vel_x3, vel_x1);
}
#[test]
fn test_process_as_mouse_diagonal() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 255, 0, &config);
assert!(result.is_some(), "Diagonal should return velocity");
let (vel_x, vel_y) = result.unwrap();
assert!(vel_x > 0, "Diagonal up-right should have positive X velocity");
assert!(vel_y > 0, "Diagonal up-right should have positive Y velocity");
let result = processor.process_as_mouse(&calibration, 0, 255, &config);
assert!(result.is_some(), "Diagonal should return velocity");
let (vel_x, vel_y) = result.unwrap();
assert!(vel_x < 0, "Diagonal down-left should have negative X velocity");
assert!(vel_y < 0, "Diagonal down-left should have negative Y velocity");
}
#[test]
fn test_process_as_mouse_deadzone_filters_small_movements() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.2; let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 135, 128, &config);
assert!(result.is_none(), "Small X movement should be filtered by deadzone");
let result = processor.process_as_mouse(&calibration, 128, 135, &config);
assert!(result.is_none(), "Small Y movement should be filtered by deadzone");
}
#[test]
fn test_mouse_velocity_config_default() {
let config = MouseVelocityConfig::default();
assert_eq!(config.multiplier, 10.0, "Default multiplier should be 10.0");
}
#[test]
fn test_process_as_mouse_axis_inversion() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
let config = MouseVelocityConfig::default();
let result = processor.process_as_mouse(&calibration, 255, 128, &config);
assert!(result.is_some());
let (vel_x, _) = result.unwrap();
assert!(vel_x > 0, "Normal X should be positive");
calibration.invert_x = true;
let result = processor.process_as_mouse(&calibration, 255, 128, &config);
assert!(result.is_some());
let (vel_x, _) = result.unwrap();
assert!(vel_x < 0, "Inverted X should be negative");
calibration.invert_x = false;
calibration.invert_y = false;
let result = processor.process_as_mouse(&calibration, 128, 0, &config);
assert!(result.is_some());
let (_, vel_y1) = result.unwrap();
assert!(vel_y1 > 0, "Normal Y=0 should be positive (up)");
calibration.invert_y = true;
let result = processor.process_as_mouse(&calibration, 128, 0, &config);
assert!(result.is_some());
let (_, vel_y2) = result.unwrap();
assert!(vel_y2 < 0, "Inverted Y should flip direction");
}
#[test]
fn test_process_as_camera_scroll_mode_deadzone() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let result = processor.process_as_camera(&calibration, 128, 128, CameraOutputMode::Scroll);
assert!(result.is_none(), "Center position should return None in Scroll mode");
}
#[test]
fn test_process_as_camera_scroll_mode_up() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let result = processor.process_as_camera(&calibration, 128, 0, CameraOutputMode::Scroll);
assert!(result.is_some(), "Full up should return output");
match result.unwrap() {
CameraOutput::Scroll(amount) => {
assert!(amount > 0, "Full up should have positive scroll amount, got {}", amount);
}
_ => panic!("Should return Scroll variant"),
}
}
#[test]
fn test_process_as_camera_scroll_mode_down() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let result = processor.process_as_camera(&calibration, 128, 255, CameraOutputMode::Scroll);
assert!(result.is_some(), "Full down should return output");
match result.unwrap() {
CameraOutput::Scroll(amount) => {
assert!(amount < 0, "Full down should have negative scroll amount, got {}", amount);
}
_ => panic!("Should return Scroll variant"),
}
}
#[test]
fn test_process_as_camera_key_mode_all_directions() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.0;
let result = processor.process_as_camera(&calibration, 128, 0, CameraOutputMode::Keys);
assert!(result.is_some());
match result.unwrap() {
CameraOutput::Keys(keys) => {
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_PAGEUP);
}
_ => panic!("Should return Keys variant"),
}
let result = processor.process_as_camera(&calibration, 128, 255, CameraOutputMode::Keys);
assert!(result.is_some());
match result.unwrap() {
CameraOutput::Keys(keys) => {
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_PAGEDOWN);
}
_ => panic!("Should return Keys variant"),
}
let result = processor.process_as_camera(&calibration, 0, 128, CameraOutputMode::Keys);
assert!(result.is_some());
match result.unwrap() {
CameraOutput::Keys(keys) => {
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_LEFT);
}
_ => panic!("Should return Keys variant"),
}
let result = processor.process_as_camera(&calibration, 255, 128, CameraOutputMode::Keys);
assert!(result.is_some());
match result.unwrap() {
CameraOutput::Keys(keys) => {
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_RIGHT);
}
_ => panic!("Should return Keys variant"),
}
let result = processor.process_as_camera(&calibration, 255, 0, CameraOutputMode::Keys);
assert!(result.is_some());
match result.unwrap() {
CameraOutput::Keys(keys) => {
assert_eq!(keys.len(), 2);
assert!(keys.contains(&Key::KEY_PAGEUP));
assert!(keys.contains(&Key::KEY_RIGHT));
}
_ => panic!("Should return Keys variant"),
}
let result = processor.process_as_camera(&calibration, 0, 255, CameraOutputMode::Keys);
assert!(result.is_some());
match result.unwrap() {
CameraOutput::Keys(keys) => {
assert_eq!(keys.len(), 2);
assert!(keys.contains(&Key::KEY_PAGEDOWN));
assert!(keys.contains(&Key::KEY_LEFT));
}
_ => panic!("Should return Keys variant"),
}
}
#[test]
fn test_process_as_camera_sensitivity_affects_scroll() {
let processor = AnalogProcessor::new();
let mut calibration_low = AnalogCalibration::default();
calibration_low.sensitivity_multiplier = 0.5;
let mut calibration_high = AnalogCalibration::default();
calibration_high.sensitivity_multiplier = 2.0;
let result_low = processor.process_as_camera(&calibration_low, 128, 0, CameraOutputMode::Scroll);
assert!(result_low.is_some());
let amount_low = match result_low.unwrap() {
CameraOutput::Scroll(amount) => amount,
_ => panic!("Should return Scroll variant"),
};
let result_high = processor.process_as_camera(&calibration_high, 128, 0, CameraOutputMode::Scroll);
assert!(result_high.is_some());
let amount_high = match result_high.unwrap() {
CameraOutput::Scroll(amount) => amount,
_ => panic!("Should return Scroll variant"),
};
assert!(amount_high.abs() > amount_low.abs(),
"Higher sensitivity should give larger scroll: {} > {}",
amount_high.abs(), amount_low.abs());
}
#[test]
fn test_camera_direction_to_keys() {
let keys = camera_direction_to_keys(DpadDirection::None);
assert!(keys.is_empty(), "None should return empty Vec");
let keys = camera_direction_to_keys(DpadDirection::Up);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_PAGEUP, "Up should map to PageUp");
let keys = camera_direction_to_keys(DpadDirection::Down);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_PAGEDOWN, "Down should map to PageDown");
let keys = camera_direction_to_keys(DpadDirection::Left);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_LEFT, "Left should map to Left arrow");
let keys = camera_direction_to_keys(DpadDirection::Right);
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Key::KEY_RIGHT, "Right should map to Right arrow");
let keys = camera_direction_to_keys(DpadDirection::UpRight);
assert_eq!(keys.len(), 2);
assert!(keys.contains(&Key::KEY_PAGEUP), "UpRight should contain PageUp");
assert!(keys.contains(&Key::KEY_RIGHT), "UpRight should contain Right");
let keys = camera_direction_to_keys(DpadDirection::DownLeft);
assert_eq!(keys.len(), 2);
assert!(keys.contains(&Key::KEY_PAGEDOWN), "DownLeft should contain PageDown");
assert!(keys.contains(&Key::KEY_LEFT), "DownLeft should contain Left");
}
#[test]
fn test_process_as_camera_key_mode_deadzone() {
let processor = AnalogProcessor::new();
let calibration = AnalogCalibration::default();
let result = processor.process_as_camera(&calibration, 128, 128, CameraOutputMode::Keys);
assert!(result.is_none(), "Center should be filtered by deadzone in Keys mode");
let result = processor.process_as_camera(&calibration, 135, 128, CameraOutputMode::Keys);
assert!(result.is_none(), "Small movement should be filtered by deadzone");
}
#[test]
fn test_process_as_camera_deadzone_filters_small_movements() {
let processor = AnalogProcessor::new();
let mut calibration = AnalogCalibration::default();
calibration.deadzone = 0.2;
let result = processor.process_as_camera(&calibration, 135, 128, CameraOutputMode::Scroll);
assert!(result.is_none(), "Small X movement should be filtered in Scroll mode");
let result = processor.process_as_camera(&calibration, 128, 135, CameraOutputMode::Keys);
assert!(result.is_none(), "Small Y movement should be filtered in Keys mode");
}
}