diff --git a/Cargo.toml b/Cargo.toml index 1adfdc5..ca94797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "joycon-rs" -version = "0.5.0-alpha" +version = "0.5.1" authors = ["Kaisei Yokoyama "] repository = "https://github.com/KaiseiYokoyama/joycon-rs" edition = "2018" description = " a framework for dealing with Nintendo Switch Joy-Con on Rust easily and efficiently" readme = "README.md" +categories = ["game-development", "hardware-support"] keywords = ["nintendo_switch", "joycon", "gamedev", "hid", "bluetooth"] license = "Apache-2.0" exclude = ["/images/*"] diff --git a/examples/scan_for_joycons.rs b/examples/scan_for_joycons.rs index 37bf2ba..aaea07f 100644 --- a/examples/scan_for_joycons.rs +++ b/examples/scan_for_joycons.rs @@ -19,6 +19,7 @@ fn main() -> JoyConResult<()> { devices.iter() .try_for_each::<_, JoyConResult<()>>(|d| { if let Ok(device) = d.lock() { + dbg!(&device); let device: &HidDevice = device.deref().try_into()?; println!("{:?}", device.get_product_string()?); } diff --git a/src/joycon/device.rs b/src/joycon/device.rs index 08fc63a..03c3711 100644 --- a/src/joycon/device.rs +++ b/src/joycon/device.rs @@ -8,10 +8,505 @@ pub enum JoyConDeviceType { ProCon = 2, } +pub mod calibration { + use super::*; + + pub mod stick { + use super::*; + + #[derive(Debug, Clone, Hash, Eq, PartialEq)] + pub struct AxisCalibration { + max: i16, + center: i16, + min: i16, + } + + impl AxisCalibration { + /// Max above center + pub fn max(&self) -> i16 { + self.max + } + + /// Center + pub fn center(&self) -> i16 { + self.center + } + + /// Min above center + pub fn min(&self) -> i16 { + self.min + } + } + + #[derive(Debug, Clone, Hash, Eq, PartialEq)] + pub enum StickCalibration { + Available { + x: AxisCalibration, + y: AxisCalibration, + }, + Unavailable, + } + + #[derive(Debug, Clone, Hash, Eq, PartialEq)] + pub struct JoyConSticksCalibration { + left: StickCalibration, + right: StickCalibration, + } + + impl JoyConSticksCalibration { + pub fn left(&self) -> &StickCalibration { + &self.left + } + + pub fn right(&self) -> &StickCalibration { + &self.right + } + } + + impl From<[u8; 18]> for JoyConSticksCalibration { + fn from(stick_cal: [u8; 18]) -> Self { + fn stick_cal_to_data(stick_cal: &[u8]) -> [u16; 6] { + let mut data = [0u16; 6]; + + data[0] = ((stick_cal[1] as u16) << 8) & 0xF00 | stick_cal[0] as u16; + data[1] = ((stick_cal[2] as u16) << 4) | ((stick_cal[1] as u16) >> 4); + data[2] = ((stick_cal[4] as u16) << 8) & 0xF00 | stick_cal[3] as u16; + data[3] = ((stick_cal[5] as u16) << 4) | ((stick_cal[4] as u16) >> 4); + data[4] = ((stick_cal[7] as u16) << 8) & 0xF00 | stick_cal[6] as u16; + data[5] = ((stick_cal[8] as u16) << 4) | ((stick_cal[7] as u16) >> 4); + + data + } + + let left_stick_calibration = { + let left_stick_cal = &stick_cal[0..9]; + if left_stick_cal.iter().all(|u| u == &0xFF) + { + StickCalibration::Unavailable + } else { + let left_stick_data = stick_cal_to_data(&stick_cal[0..9]); + + StickCalibration::Available { + x: AxisCalibration { + center: left_stick_data[2] as i16, + max: left_stick_data[2] as i16 + left_stick_data[0] as i16, + min: left_stick_data[2] as i16 - left_stick_data[4] as i16, + }, + y: AxisCalibration { + center: left_stick_data[3] as i16, + max: left_stick_data[3] as i16 + left_stick_data[1] as i16, + min: left_stick_data[3] as i16 - left_stick_data[5] as i16, + }, + } + } + }; + + let right_stick_calibration = { + let right_stick_cal = &stick_cal[9..18]; + if right_stick_cal.iter().all(|u| u == &0xFF) + { + StickCalibration::Unavailable + } else { + let right_stick_data = stick_cal_to_data(right_stick_cal); + + StickCalibration::Available { + x: AxisCalibration { + center: right_stick_data[0] as i16, + max: right_stick_data[0] as i16 + right_stick_data[4] as i16, + min: right_stick_data[0] as i16 - right_stick_data[2] as i16, + }, + y: AxisCalibration { + center: right_stick_data[1] as i16, + max: right_stick_data[1] as i16 + right_stick_data[5] as i16, + min: right_stick_data[1] as i16 - right_stick_data[3] as i16, + }, + } + } + }; + + JoyConSticksCalibration { + left: left_stick_calibration, + right: right_stick_calibration, + } + } + } + + pub fn get_factory_calibration(device: &HidDevice) -> Option { + device.write(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x3D, 0x60, 0, 0, 18]) + .ok()?; + + for _ in 0..5 { + let mut buf = [0u8; 64]; + device.read_timeout(&mut buf, 20) + .ok()?; + + match buf[14..20] { + [0x10, 0x3D, 0x60, 0, 0, 18] => {} + _ => continue, + } + + let mut report = [0u8; 18]; + report.copy_from_slice(&buf[20..38]); + + return Some(JoyConSticksCalibration::from(report)); + } + + None + } + + pub fn get_user_calibration(device: &HidDevice) -> Option { + device.write(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x12, 0x80, 0, 0, 20]) + .ok()?; + + for _ in 0..5 { + let mut buf = [0u8; 64]; + device.read_timeout(&mut buf, 20) + .ok()?; + + match buf[14..20] { + [0x10, 0x12, 0x80, 0, 0, 20] => {} + _ => continue, + } + + let mut report = [0u8; 18]; + { + let (left, right) = report.split_at_mut(9); + left.copy_from_slice(&buf[20..29]); + right.copy_from_slice(&buf[31..40]); + } + + return Some(JoyConSticksCalibration::from(report)); + } + + None + } + + #[derive(Debug, Clone, Hash, Eq, PartialEq)] + pub struct StickParameters { + dead_zone: u16, + range_ratio: u16, + } + + impl StickParameters { + pub fn dead_zone(&self) -> u16 { + self.dead_zone + } + + pub fn range_ratio(&self) -> u16 { + self.range_ratio + } + } + + impl From<[u8; 18]> for StickParameters { + fn from(array: [u8; 18]) -> Self { + fn decode(stick_cal: &[u8]) -> [u16; 12] { + let mut data = [0u16; 12]; + + data[0] = ((stick_cal[1] as u16) << 8) & 0xF00 | stick_cal[0] as u16; + data[1] = ((stick_cal[2] as u16) << 4) | ((stick_cal[1] as u16) >> 4); + data[2] = ((stick_cal[4] as u16) << 8) & 0xF00 | stick_cal[3] as u16; + data[3] = ((stick_cal[5] as u16) << 4) | ((stick_cal[4] as u16) >> 4); + data[4] = ((stick_cal[7] as u16) << 8) & 0xF00 | stick_cal[6] as u16; + data[5] = ((stick_cal[8] as u16) << 4) | ((stick_cal[7] as u16) >> 4); + data[6] = ((stick_cal[10] as u16) << 8) & 0xF00 | stick_cal[9] as u16; + data[7] = ((stick_cal[11] as u16) << 4) | ((stick_cal[10] as u16) >> 4); + data[8] = ((stick_cal[13] as u16) << 8) & 0xF00 | stick_cal[12] as u16; + data[9] = ((stick_cal[14] as u16) << 4) | ((stick_cal[13] as u16) >> 4); + data[10] = ((stick_cal[16] as u16) << 8) & 0xF00 | stick_cal[15] as u16; + data[11] = ((stick_cal[17] as u16) << 4) | ((stick_cal[16] as u16) >> 4); + + data + } + + let decoded = decode(&array); + + StickParameters { + dead_zone: decoded[2], + range_ratio: decoded[3], + } + } + } + + pub fn get_parameters(device: &HidDevice) -> Option { + device.write(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x86, 0x60, 0, 0, 18]) + .ok()?; + + for _ in 0..5 { + let mut buf = [0u8; 64]; + device.read_timeout(&mut buf, 20) + .ok()?; + + match buf[14..20] { + [0x10, 0x86, 0x60, 0, 0, 18] => {} + _ => continue, + } + + let mut report = [0u8; 18]; + report.copy_from_slice(&buf[20..38]); + + return Some(StickParameters::from(report)); + } + + None + } + } + + pub mod imu { + use super::*; + use std::fmt::Debug; + use std::hash::Hash; + + #[derive(Debug)] + pub struct XYZ { + pub x: T, + pub y: T, + pub z: T, + } + + impl Clone for XYZ where T: Debug + Clone { + fn clone(&self) -> Self { + Self { + x: self.x.clone(), + y: self.y.clone(), + z: self.z.clone(), + } + } + } + + impl Hash for XYZ where T: Debug + Hash { + fn hash(&self, state: &mut H) { + self.x.hash(state); + self.y.hash(state); + self.z.hash(state); + } + } + + impl PartialEq for XYZ where T: Debug + PartialEq { + fn eq(&self, other: &Self) -> bool { + self.x == other.x + && self.y == other.y + && self.z == other.z + } + } + + impl Eq for XYZ where T: Debug + Eq {} + + #[derive(Debug, Clone, Hash, Eq, PartialEq)] + pub enum IMUCalibration { + Available { + /// Acc XYZ origin position when completely horizontal and stick is upside + acc_origin_position: XYZ, + /// Acc XYZ sensitivity special coeff, for default sensitivity: ±8G. + acc_sensitivity_special_coeff: XYZ, + /// Gyro XYZ origin position when still + gyro_origin_position: XYZ, + /// Gyro XYZ sensitivity special coeff, for default sensitivity: ±2000dps. + gyro_sensitivity_special_coeff: XYZ, + }, + Unavailable, + } + + impl From<[u8; 24]> for IMUCalibration { + fn from(value: [u8; 24]) -> Self { + use std::slice::Iter; + use std::iter::Cloned; + + if value.iter().all(|v| v == &0xFF) { + return IMUCalibration::Unavailable; + } + + fn convert(little: u8, big: u8) -> i16 { + i16::from_be_bytes([big, little]) + } + + fn iter_to_xyz_i16(iter: &mut Cloned>) -> XYZ { + let x = convert(iter.next().unwrap(), iter.next().unwrap()); + let y = convert(iter.next().unwrap(), iter.next().unwrap()); + let z = convert(iter.next().unwrap(), iter.next().unwrap()); + + XYZ { x, y, z } + } + + let mut iter = value.iter().cloned(); + + let acc_origin_position = iter_to_xyz_i16(&mut iter); + let acc_sensitivity_special_coeff = iter_to_xyz_i16(&mut iter); + let gyro_origin_position = iter_to_xyz_i16(&mut iter); + let gyro_sensitivity_special_coeff = iter_to_xyz_i16(&mut iter); + + IMUCalibration::Available { + acc_origin_position, + acc_sensitivity_special_coeff, + gyro_origin_position, + gyro_sensitivity_special_coeff, + } + } + } + + pub fn get_factory_calibration(device: &HidDevice) -> Option { + device.write(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x60, 0, 0, 24]) + .ok()?; + + for _ in 0..5 { + let mut buf = [0u8; 64]; + device.read_timeout(&mut buf, 20) + .ok()?; + + match buf[14..20] { + [0x10, 0x20, 0x60, 0, 0, 24] => {} + _ => continue, + } + + let mut report = [0u8; 24]; + report.copy_from_slice(&buf[20..44]); + + return Some(IMUCalibration::from(report)); + } + + None + } + + pub fn get_user_calibration(device: &HidDevice) -> Option { + device.write(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x28, 0x80, 0, 0, 24]) + .ok()?; + + for _ in 0..5 { + let mut buf = [0u8; 64]; + device.read_timeout(&mut buf, 20) + .ok()?; + + match buf[14..20] { + [0x10, 0x28, 0x80, 0, 0, 24] => {} + _ => continue, + } + + let mut report = [0u8; 24]; + report.copy_from_slice(&buf[20..44]); + + return Some(IMUCalibration::from(report)); + } + + None + } + + #[derive(Debug, Clone, Hash, Eq, PartialEq)] + pub struct IMUOffsets { + x: i16, + y: i16, + z: i16, + } + + impl From<[u8; 6]> for IMUOffsets { + fn from(array: [u8; 6]) -> Self { + let x = i16::from_le_bytes([array[0], array[1]]); + let y = i16::from_le_bytes([array[2], array[3]]); + let z = i16::from_le_bytes([array[4], array[5]]); + + IMUOffsets { + x, + y, + z, + } + } + } + + pub fn get_offsets(device: &HidDevice) -> Option { + device.write(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x80, 0x60, 0, 0, 6]) + .ok()?; + + for _ in 0..5 { + let mut buf = [0u8; 64]; + device.read_timeout(&mut buf, 20) + .ok()?; + + match buf[14..20] { + [0x10, 0x80, 0x60, 0, 0, 6] => {} + _ => continue, + } + + let mut report = [0u8; 6]; + report.copy_from_slice(&buf[20..26]); + + return Some(IMUOffsets::from(report)); + } + + None + } + } +} + +pub mod color { + use super::*; + + #[derive(Debug, Clone, Hash, Eq, PartialEq)] + pub struct Color { + /// Controller body color. ex. [30, 220, 0] (Splatoon2 green) + pub body: [u8; 3], + pub buttons: [u8; 3], + pub left_grip: Option<[u8; 3]>, + pub right_grip: Option<[u8; 3]>, + } + + impl From<[u8; 12]> for Color { + fn from(array: [u8; 12]) -> Self { + let body = [array[0], array[1], array[2]]; + let buttons = [array[3], array[4], array[5]]; + let left_grip = if array[6..9].iter().all(|a| a == &0xFF) { + None + } else { + Some([array[6], array[7], array[8]]) + }; + let right_grip = if array[9..12].iter().all(|a| a == &0xFF) { + None + } else { + Some([array[9], array[10], array[11]]) + }; + + Color { + body, + buttons, + left_grip, + right_grip, + } + } + } + + pub fn get_color(device: &HidDevice) -> Option { + device.write(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x50, 0x60, 0, 0, 12]) + .ok()?; + + for _ in 0..5 { + let mut buf = [0u8; 64]; + device.read_timeout(&mut buf, 20) + .ok()?; + + match buf[14..20] { + [0x10, 0x50, 0x60, 0, 0, 12] => {} + _ => continue, + } + + let mut report = [0u8; 12]; + report.copy_from_slice(&buf[20..32]); + + return Some(Color::from(report)); + } + + None + } +} + + pub struct JoyConDevice { hid_device: Option, serial_number: String, device_type: JoyConDeviceType, + stick_parameters: calibration::stick::StickParameters, + stick_factory_calibration: calibration::stick::JoyConSticksCalibration, + stick_user_calibration: calibration::stick::JoyConSticksCalibration, + imu_offsets: calibration::imu::IMUOffsets, + imu_factory_calibration: calibration::imu::IMUCalibration, + imu_user_calibration: calibration::imu::IMUCalibration, + color: color::Color, } impl JoyConDevice { @@ -41,6 +536,24 @@ impl JoyConDevice { &self.serial_number } + pub fn stick_factory_calibration(&self) -> &calibration::stick::JoyConSticksCalibration { + &self.stick_factory_calibration + } + + pub fn stick_user_calibration(&self) -> &calibration::stick::JoyConSticksCalibration { + &self.stick_user_calibration + } + + pub fn imu_factory_calibration(&self) -> &calibration::imu::IMUCalibration { + &self.imu_factory_calibration + } + + pub fn imu_user_calibration(&self) -> &calibration::imu::IMUCalibration { + &self.imu_user_calibration + } + + pub fn color(&self) -> &color::Color { &self.color } + /// Set blocking mode. /// /// # Notice @@ -73,12 +586,33 @@ impl JoyConDevice { let hid_device = hidapi.open_serial(device_info.vendor_id(), device_info.product_id(), serial)?; + let stick_parameters = calibration::stick::get_parameters(&hid_device) + .ok_or(JoyConDeviceError::FailedStickParameterLoading)?; + let stick_factory_calibration = calibration::stick::get_factory_calibration(&hid_device) + .ok_or(JoyConDeviceError::FailedStickCalibrationLoading)?; + let stick_user_calibration = calibration::stick::get_user_calibration(&hid_device) + .ok_or(JoyConDeviceError::FailedStickCalibrationLoading)?; + let imu_offsets = calibration::imu::get_offsets(&hid_device) + .ok_or(JoyConDeviceError::FailedIMUOffsetsLoading)?; + let imu_factory_calibration = calibration::imu::get_factory_calibration(&hid_device) + .ok_or(JoyConDeviceError::FailedIMUCalibrationLoading)?; + let imu_user_calibration = calibration::imu::get_user_calibration(&hid_device) + .ok_or(JoyConDeviceError::FailedIMUCalibrationLoading)?; + let color = color::get_color(&hid_device) + .ok_or(JoyConDeviceError::FailedColorLoading)?; Ok( JoyConDevice { hid_device: Some(hid_device), serial_number: serial.to_string(), device_type, + stick_parameters, + stick_factory_calibration, + stick_user_calibration, + imu_offsets, + imu_factory_calibration, + imu_user_calibration, + color, } ) } @@ -123,12 +657,19 @@ impl JoyConDevice { impl Debug for JoyConDevice { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "JoyConDevice {{ hid_device: {}, serial_number: {}, device_type: {:?} }}", + writeln!(f, "JoyConDevice {{ hid_device: {}, serial_number: {}, device_type: {:?}, stick_parameters: {:?}, , stick_factory_calibration: {:?}, stick_user_calibration: {:?}, imu_offsets: {:?}, imu_factory_calibration: {:?}, imu_user_calibration: {:?}, color: {:?} }}", if self.is_connected() { "Connected" } else { "Disconnected" }, &self.serial_number, - &self.device_type + &self.device_type, + &self.stick_parameters, + &self.stick_factory_calibration, + &self.stick_user_calibration, + &self.imu_offsets, + &self.imu_factory_calibration, + &self.imu_user_calibration, + &self.color, ) } } diff --git a/src/lib.rs b/src/lib.rs index 8d8bd95..630d49b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,6 +186,11 @@ pub mod result { pub enum JoyConDeviceError { InvalidVendorID(u16), InvalidProductID(u16), + FailedStickParameterLoading, + FailedStickCalibrationLoading, + FailedIMUOffsetsLoading, + FailedIMUCalibrationLoading, + FailedColorLoading, } impl From for JoyConError {