Skip to content

Commit

Permalink
feat(SourceDevice): add Nintendo Switch controller support
Browse files Browse the repository at this point in the history
  • Loading branch information
ShadowApex committed Jul 27, 2024
1 parent b545cd9 commit 8b33292
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 9 deletions.
12 changes: 8 additions & 4 deletions rootfs/usr/share/inputplumber/devices/60-switch_pro.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ matches: []
# One or more source devices to combine into a single virtual device. The events
# from these devices will be watched and translated according to the key map.
source_devices:
- group: gamepad
evdev:
name: Nintendo Co., Ltd. Pro Controller
handler: event*
#- group: gamepad
# evdev:
# name: Nintendo Co., Ltd. Pro Controller
# handler: event*
#- group: imu
# evdev:
# name: Nintendo Co., Ltd. Pro Controller (IMU)
- group: gamepad
hidraw:
vendor_id: 0x057e
product_id: 0x2009

# The target input device(s) that the virtual device profile can use
target_devices:
Expand Down
1 change: 1 addition & 0 deletions src/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod iio_imu;
pub mod lego;
pub mod opineo;
pub mod steam_deck;
pub mod switch;
107 changes: 107 additions & 0 deletions src/drivers/switch/driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::{error::Error, ffi::CString};

use hidapi::HidDevice;
use packed_struct::prelude::*;

use super::{
event::Event,
hid_report::{PackedInputDataReport, ReportType},
};

// Hardware IDs
pub const VID: u16 = 0x057e;
pub const PID: u16 = 0x2009;

/// Size of the HID packet
const PACKET_SIZE: usize = 64 + 35;

/// Nintendo Switch input driver
pub struct Driver {
state: Option<PackedInputDataReport>,
device: HidDevice,
}

impl Driver {
pub fn new(path: String) -> Result<Self, Box<dyn Error + Send + Sync>> {
let path = CString::new(path)?;
let api = hidapi::HidApi::new()?;
let device = api.open_path(&path)?;
let info = device.get_device_info()?;
if info.vendor_id() != VID || info.product_id() != PID {
return Err("Device '{path}' is not a Switch Controller".into());
}

Ok(Self {
device,
state: None,
})
}

/// Poll the device and read input reports
pub fn poll(&mut self) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> {
log::debug!("Polling device");

// Read data from the device into a buffer
let mut buf = [0; PACKET_SIZE];
let bytes_read = self.device.read(&mut buf[..])?;

// Handle the incoming input report
let events = self.handle_input_report(buf, bytes_read)?;

Ok(events)
}

/// Unpacks the buffer into a [PackedInputDataReport] structure and updates
/// the internal gamepad state
fn handle_input_report(
&mut self,
buf: [u8; PACKET_SIZE],
bytes_read: usize,
) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> {
// Read the report id
let report_id = buf[0];
let report_type = ReportType::try_from(report_id)?;
log::debug!("Received report: {report_type:?}");

let slice = &buf[..bytes_read];
match report_type {
ReportType::CommandOutputReport => todo!(),
ReportType::McuUpdateOutputReport => todo!(),
ReportType::BasicOutputReport => todo!(),
ReportType::McuOutputReport => todo!(),
ReportType::AttachmentOutputReport => todo!(),
ReportType::CommandInputReport => todo!(),
ReportType::McuUpdateInputReport => todo!(),
ReportType::BasicInputReport => {
let sized_buf = slice.try_into()?;
let input_report = PackedInputDataReport::unpack(sized_buf)?;

// Print input report for debugging
log::debug!("--- Input report ---");
log::debug!("{input_report}");
log::debug!("---- End Report ----");
}
ReportType::McuInputReport => todo!(),
ReportType::AttachmentInputReport => todo!(),
ReportType::Unused1 => todo!(),
ReportType::GenericInputReport => todo!(),
ReportType::OtaEnableFwuReport => todo!(),
ReportType::OtaSetupReadReport => todo!(),
ReportType::OtaReadReport => todo!(),
ReportType::OtaWriteReport => todo!(),
ReportType::OtaEraseReport => todo!(),
ReportType::OtaLaunchReport => todo!(),
ReportType::ExtGripOutputReport => todo!(),
ReportType::ExtGripInputReport => todo!(),
ReportType::Unused2 => todo!(),
}

// Update the state
//let old_state = self.update_state(input_report);

// Translate the state into a stream of input events
//let events = self.translate(old_state);

Ok(vec![])
}
}
1 change: 1 addition & 0 deletions src/drivers/switch/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub struct Event {}
205 changes: 205 additions & 0 deletions src/drivers/switch/hid_report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//! Sources:
//! - https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md
//! - https://github.com/torvalds/linux/blob/master/drivers/hid/hid-nintendo.c
//! - https://switchbrew.org/w/index.php?title=Joy-Con
use packed_struct::prelude::*;

#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug)]
pub enum ReportType {
CommandOutputReport = 0x01,
McuUpdateOutputReport = 0x03,
BasicOutputReport = 0x10,
McuOutputReport = 0x11,
AttachmentOutputReport = 0x12,
CommandInputReport = 0x21,
McuUpdateInputReport = 0x23,
BasicInputReport = 0x30,
McuInputReport = 0x31,
AttachmentInputReport = 0x32,
Unused1 = 0x33,
GenericInputReport = 0x3F,
OtaEnableFwuReport = 0x70,
OtaSetupReadReport = 0x71,
OtaReadReport = 0x72,
OtaWriteReport = 0x73,
OtaEraseReport = 0x74,
OtaLaunchReport = 0x75,
ExtGripOutputReport = 0x80,
ExtGripInputReport = 0x81,
Unused2 = 0x82,
}

impl TryFrom<u8> for ReportType {
type Error = &'static str;

fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x01 => Ok(Self::CommandOutputReport),
0x03 => Ok(Self::McuUpdateOutputReport),
0x10 => Ok(Self::BasicOutputReport),
0x11 => Ok(Self::McuOutputReport),
0x12 => Ok(Self::AttachmentOutputReport),
0x21 => Ok(Self::CommandInputReport),
0x23 => Ok(Self::McuUpdateInputReport),
0x30 => Ok(Self::BasicInputReport),
0x31 => Ok(Self::McuInputReport),
0x32 => Ok(Self::AttachmentInputReport),
0x33 => Ok(Self::Unused1),
0x3F => Ok(Self::GenericInputReport),
0x70 => Ok(Self::OtaEnableFwuReport),
0x71 => Ok(Self::OtaSetupReadReport),
0x72 => Ok(Self::OtaReadReport),
0x73 => Ok(Self::OtaWriteReport),
0x74 => Ok(Self::OtaEraseReport),
0x75 => Ok(Self::OtaLaunchReport),
0x80 => Ok(Self::ExtGripOutputReport),
0x81 => Ok(Self::ExtGripInputReport),
0x82 => Ok(Self::Unused2),
_ => Err("Invalid report type"),
}
}
}

#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug)]
pub enum BatteryLevel {
Empty = 0,
Critical = 1,
Low = 2,
Medium = 3,
Full = 4,
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "1")]
pub struct BatteryConnection {
/// Battery level. 8=full, 6=medium, 4=low, 2=critical, 0=empty. LSB=Charging.
#[packed_field(bits = "0..=2", ty = "enum")]
pub battery_level: BatteryLevel,
#[packed_field(bits = "3")]
pub charging: bool,
/// Connection info. (con_info >> 1) & 3 - 3=JC, 0=Pro/ChrGrip. con_info & 1 - 1=Switch/USB powered.
#[packed_field(bits = "4..=7")]
pub conn_info: u8,
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "3")]
pub struct ButtonStatus {
// byte 0 (Right)
#[packed_field(bits = "7")]
pub y: bool,
#[packed_field(bits = "6")]
pub x: bool,
#[packed_field(bits = "5")]
pub b: bool,
#[packed_field(bits = "4")]
pub a: bool,
#[packed_field(bits = "3")]
pub sr_right: bool,
#[packed_field(bits = "2")]
pub sl_right: bool,
#[packed_field(bits = "1")]
pub r: bool,
#[packed_field(bits = "0")]
pub zr: bool,

// byte 1 (Shared)
#[packed_field(bits = "15")]
pub minus: bool,
#[packed_field(bits = "14")]
pub plus: bool,
#[packed_field(bits = "13")]
pub r_stick: bool,
#[packed_field(bits = "12")]
pub l_stick: bool,
#[packed_field(bits = "11")]
pub home: bool,
#[packed_field(bits = "10")]
pub capture: bool,
#[packed_field(bits = "9")]
pub _unused: bool,
#[packed_field(bits = "8")]
pub charging_grip: bool,

// byte 2 (Left)
#[packed_field(bits = "23")]
pub down: bool,
#[packed_field(bits = "22")]
pub up: bool,
#[packed_field(bits = "21")]
pub right: bool,
#[packed_field(bits = "20")]
pub left: bool,
#[packed_field(bits = "19")]
pub sr_left: bool,
#[packed_field(bits = "18")]
pub sl_left: bool,
#[packed_field(bits = "17")]
pub l: bool,
#[packed_field(bits = "16")]
pub zl: bool,
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "3")]
pub struct StickData {
/// Analog stick X-axis
#[packed_field(bits = "0..=11", endian = "msb")]
pub y: Integer<i16, packed_bits::Bits<12>>,
/// Analog stick Y-axis
#[packed_field(bits = "12..=23", endian = "msb")]
pub x: Integer<i16, packed_bits::Bits<12>>,
}

/// The 6-Axis data is repeated 3 times. On Joy-con with a 15ms packet push,
/// this is translated to 5ms difference sampling. E.g. 1st sample 0ms, 2nd 5ms,
/// 3rd 10ms. Using all 3 samples let you have a 5ms precision instead of 15ms.
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "12")]
pub struct ImuData {
#[packed_field(bytes = "0..=1", endian = "lsb")]
pub accel_x: Integer<i16, packed_bits::Bits<16>>,
#[packed_field(bytes = "2..=3", endian = "lsb")]
pub accel_y: Integer<i16, packed_bits::Bits<16>>,
#[packed_field(bytes = "4..=5", endian = "lsb")]
pub accel_z: Integer<i16, packed_bits::Bits<16>>,
#[packed_field(bytes = "6..=7", endian = "lsb")]
pub gyro_x: Integer<i16, packed_bits::Bits<16>>,
#[packed_field(bytes = "8..=9", endian = "lsb")]
pub gyro_y: Integer<i16, packed_bits::Bits<16>>,
#[packed_field(bytes = "10..=11", endian = "lsb")]
pub gyro_z: Integer<i16, packed_bits::Bits<16>>,
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "64")]
pub struct PackedInputDataReport {
// byte 0-2
/// Input report ID
#[packed_field(bytes = "0", ty = "enum")]
pub id: ReportType,
/// Timer. Increments very fast. Can be used to estimate excess Bluetooth latency.
#[packed_field(bytes = "1")]
pub timer: u8,
/// Battery and connection information
#[packed_field(bytes = "2")]
pub info: BatteryConnection,

// byte 3-5
/// Button status
#[packed_field(bytes = "3..=5")]
pub buttons: ButtonStatus,

// byte 6-11
/// Left analog stick
#[packed_field(bytes = "6..=8")]
pub left_stick: StickData,
/// Right analog stick
#[packed_field(bytes = "9..=11")]
pub right_stick: StickData,

// byte 12
/// Vibrator input report. Decides if next vibration pattern should be sent.
#[packed_field(bytes = "12")]
pub vibrator_report: u8,
}
4 changes: 4 additions & 0 deletions src/drivers/switch/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod driver;
pub mod event;
pub mod hid_report;
pub mod report_descriptor;
Loading

0 comments on commit 8b33292

Please sign in to comment.