Skip to content

Commit eba54bd

Browse files
committed
feat(SourceDevice): add Nintendo Switch controller support
1 parent b545cd9 commit eba54bd

File tree

11 files changed

+402
-9
lines changed

11 files changed

+402
-9
lines changed

rootfs/usr/share/inputplumber/devices/60-switch_pro.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ matches: []
1919
# One or more source devices to combine into a single virtual device. The events
2020
# from these devices will be watched and translated according to the key map.
2121
source_devices:
22-
- group: gamepad
23-
evdev:
24-
name: Nintendo Co., Ltd. Pro Controller
25-
handler: event*
22+
#- group: gamepad
23+
# evdev:
24+
# name: Nintendo Co., Ltd. Pro Controller
25+
# handler: event*
2626
#- group: imu
2727
# evdev:
2828
# name: Nintendo Co., Ltd. Pro Controller (IMU)
29+
- group: gamepad
30+
hidraw:
31+
vendor_id: 0x057e
32+
product_id: 0x2009
2933

3034
# The target input device(s) that the virtual device profile can use
3135
target_devices:

src/drivers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pub mod iio_imu;
44
pub mod lego;
55
pub mod opineo;
66
pub mod steam_deck;
7+
pub mod switch;

src/drivers/switch/driver.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::{error::Error, ffi::CString};
2+
3+
use hidapi::HidDevice;
4+
use packed_struct::prelude::*;
5+
6+
use super::{
7+
event::Event,
8+
hid_report::{PackedInputDataReport, ReportType},
9+
};
10+
11+
// Hardware IDs
12+
pub const VID: u16 = 0x057e;
13+
pub const PID: u16 = 0x2009;
14+
15+
/// Size of the HID packet
16+
const PACKET_SIZE: usize = 64 + 35;
17+
18+
/// Nintendo Switch input driver
19+
pub struct Driver {
20+
state: Option<PackedInputDataReport>,
21+
device: HidDevice,
22+
}
23+
24+
impl Driver {
25+
pub fn new(path: String) -> Result<Self, Box<dyn Error + Send + Sync>> {
26+
let path = CString::new(path)?;
27+
let api = hidapi::HidApi::new()?;
28+
let device = api.open_path(&path)?;
29+
let info = device.get_device_info()?;
30+
if info.vendor_id() != VID || info.product_id() != PID {
31+
return Err("Device '{path}' is not a Switch Controller".into());
32+
}
33+
34+
Ok(Self {
35+
device,
36+
state: None,
37+
})
38+
}
39+
40+
/// Poll the device and read input reports
41+
pub fn poll(&mut self) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> {
42+
log::debug!("Polling device");
43+
44+
// Read data from the device into a buffer
45+
let mut buf = [0; PACKET_SIZE];
46+
let bytes_read = self.device.read(&mut buf[..])?;
47+
48+
// Handle the incoming input report
49+
let events = self.handle_input_report(buf, bytes_read)?;
50+
51+
Ok(events)
52+
}
53+
54+
/// Unpacks the buffer into a [PackedInputDataReport] structure and updates
55+
/// the internal gamepad state
56+
fn handle_input_report(
57+
&mut self,
58+
buf: [u8; PACKET_SIZE],
59+
bytes_read: usize,
60+
) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> {
61+
// Read the report id
62+
let report_id = buf[0];
63+
let report_type = ReportType::try_from(report_id)?;
64+
log::debug!("Received report: {report_type:?}");
65+
66+
let slice = &buf[..bytes_read];
67+
match report_type {
68+
ReportType::CommandOutputReport => todo!(),
69+
ReportType::McuUpdateOutputReport => todo!(),
70+
ReportType::BasicOutputReport => todo!(),
71+
ReportType::McuOutputReport => todo!(),
72+
ReportType::AttachmentOutputReport => todo!(),
73+
ReportType::CommandInputReport => todo!(),
74+
ReportType::McuUpdateInputReport => todo!(),
75+
ReportType::BasicInputReport => {
76+
let sized_buf = slice.try_into()?;
77+
let input_report = PackedInputDataReport::unpack(sized_buf)?;
78+
79+
// Print input report for debugging
80+
log::debug!("--- Input report ---");
81+
log::debug!("{input_report}");
82+
log::debug!("---- End Report ----");
83+
}
84+
ReportType::McuInputReport => todo!(),
85+
ReportType::AttachmentInputReport => todo!(),
86+
ReportType::Unused1 => todo!(),
87+
ReportType::GenericInputReport => todo!(),
88+
ReportType::OtaEnableFwuReport => todo!(),
89+
ReportType::OtaSetupReadReport => todo!(),
90+
ReportType::OtaReadReport => todo!(),
91+
ReportType::OtaWriteReport => todo!(),
92+
ReportType::OtaEraseReport => todo!(),
93+
ReportType::OtaLaunchReport => todo!(),
94+
ReportType::ExtGripOutputReport => todo!(),
95+
ReportType::ExtGripInputReport => todo!(),
96+
ReportType::Unused2 => todo!(),
97+
}
98+
99+
// Update the state
100+
//let old_state = self.update_state(input_report);
101+
102+
// Translate the state into a stream of input events
103+
//let events = self.translate(old_state);
104+
105+
Ok(vec![])
106+
}
107+
}

src/drivers/switch/event.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub struct Event {}

src/drivers/switch/hid_report.rs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//! Sources:
2+
//! - https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md
3+
//! - https://github.com/torvalds/linux/blob/master/drivers/hid/hid-nintendo.c
4+
//! - https://switchbrew.org/w/index.php?title=Joy-Con
5+
use packed_struct::prelude::*;
6+
7+
#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug)]
8+
pub enum ReportType {
9+
CommandOutputReport = 0x01,
10+
McuUpdateOutputReport = 0x03,
11+
BasicOutputReport = 0x10,
12+
McuOutputReport = 0x11,
13+
AttachmentOutputReport = 0x12,
14+
CommandInputReport = 0x21,
15+
McuUpdateInputReport = 0x23,
16+
BasicInputReport = 0x30,
17+
McuInputReport = 0x31,
18+
AttachmentInputReport = 0x32,
19+
Unused1 = 0x33,
20+
GenericInputReport = 0x3F,
21+
OtaEnableFwuReport = 0x70,
22+
OtaSetupReadReport = 0x71,
23+
OtaReadReport = 0x72,
24+
OtaWriteReport = 0x73,
25+
OtaEraseReport = 0x74,
26+
OtaLaunchReport = 0x75,
27+
ExtGripOutputReport = 0x80,
28+
ExtGripInputReport = 0x81,
29+
Unused2 = 0x82,
30+
}
31+
32+
impl TryFrom<u8> for ReportType {
33+
type Error = &'static str;
34+
35+
fn try_from(value: u8) -> Result<Self, Self::Error> {
36+
match value {
37+
0x01 => Ok(Self::CommandOutputReport),
38+
0x03 => Ok(Self::McuUpdateOutputReport),
39+
0x10 => Ok(Self::BasicOutputReport),
40+
0x11 => Ok(Self::McuOutputReport),
41+
0x12 => Ok(Self::AttachmentOutputReport),
42+
0x21 => Ok(Self::CommandInputReport),
43+
0x23 => Ok(Self::McuUpdateInputReport),
44+
0x30 => Ok(Self::BasicInputReport),
45+
0x31 => Ok(Self::McuInputReport),
46+
0x32 => Ok(Self::AttachmentInputReport),
47+
0x33 => Ok(Self::Unused1),
48+
0x3F => Ok(Self::GenericInputReport),
49+
0x70 => Ok(Self::OtaEnableFwuReport),
50+
0x71 => Ok(Self::OtaSetupReadReport),
51+
0x72 => Ok(Self::OtaReadReport),
52+
0x73 => Ok(Self::OtaWriteReport),
53+
0x74 => Ok(Self::OtaEraseReport),
54+
0x75 => Ok(Self::OtaLaunchReport),
55+
0x80 => Ok(Self::ExtGripOutputReport),
56+
0x81 => Ok(Self::ExtGripInputReport),
57+
0x82 => Ok(Self::Unused2),
58+
_ => Err("Invalid report type"),
59+
}
60+
}
61+
}
62+
63+
#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug)]
64+
pub enum BatteryLevel {
65+
Empty = 0,
66+
Critical = 1,
67+
Low = 2,
68+
Medium = 3,
69+
Full = 4,
70+
}
71+
72+
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
73+
#[packed_struct(bit_numbering = "msb0", size_bytes = "1")]
74+
pub struct BatteryConnection {
75+
/// Battery level. 8=full, 6=medium, 4=low, 2=critical, 0=empty. LSB=Charging.
76+
#[packed_field(bits = "0..=2", ty = "enum")]
77+
pub battery_level: BatteryLevel,
78+
#[packed_field(bits = "3")]
79+
pub charging: bool,
80+
/// Connection info. (con_info >> 1) & 3 - 3=JC, 0=Pro/ChrGrip. con_info & 1 - 1=Switch/USB powered.
81+
#[packed_field(bits = "4..=7")]
82+
pub conn_info: u8,
83+
}
84+
85+
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
86+
#[packed_struct(bit_numbering = "msb0", size_bytes = "3")]
87+
pub struct ButtonStatus {
88+
// byte 0 (Right)
89+
#[packed_field(bits = "7")]
90+
pub y: bool,
91+
#[packed_field(bits = "6")]
92+
pub x: bool,
93+
#[packed_field(bits = "5")]
94+
pub b: bool,
95+
#[packed_field(bits = "4")]
96+
pub a: bool,
97+
#[packed_field(bits = "3")]
98+
pub sr_right: bool,
99+
#[packed_field(bits = "2")]
100+
pub sl_right: bool,
101+
#[packed_field(bits = "1")]
102+
pub r: bool,
103+
#[packed_field(bits = "0")]
104+
pub zr: bool,
105+
106+
// byte 1 (Shared)
107+
#[packed_field(bits = "15")]
108+
pub minus: bool,
109+
#[packed_field(bits = "14")]
110+
pub plus: bool,
111+
#[packed_field(bits = "13")]
112+
pub r_stick: bool,
113+
#[packed_field(bits = "12")]
114+
pub l_stick: bool,
115+
#[packed_field(bits = "11")]
116+
pub home: bool,
117+
#[packed_field(bits = "10")]
118+
pub capture: bool,
119+
#[packed_field(bits = "9")]
120+
pub _unused: bool,
121+
#[packed_field(bits = "8")]
122+
pub charging_grip: bool,
123+
124+
// byte 2 (Left)
125+
#[packed_field(bits = "23")]
126+
pub down: bool,
127+
#[packed_field(bits = "22")]
128+
pub up: bool,
129+
#[packed_field(bits = "21")]
130+
pub right: bool,
131+
#[packed_field(bits = "20")]
132+
pub left: bool,
133+
#[packed_field(bits = "19")]
134+
pub sr_left: bool,
135+
#[packed_field(bits = "18")]
136+
pub sl_left: bool,
137+
#[packed_field(bits = "17")]
138+
pub l: bool,
139+
#[packed_field(bits = "16")]
140+
pub zl: bool,
141+
}
142+
143+
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
144+
#[packed_struct(bit_numbering = "msb0", size_bytes = "3")]
145+
pub struct StickData {
146+
/// Analog stick X-axis
147+
#[packed_field(bits = "0..=11", endian = "msb")]
148+
pub y: Integer<i16, packed_bits::Bits<12>>,
149+
/// Analog stick Y-axis
150+
#[packed_field(bits = "12..=23", endian = "msb")]
151+
pub x: Integer<i16, packed_bits::Bits<12>>,
152+
}
153+
154+
/// The 6-Axis data is repeated 3 times. On Joy-con with a 15ms packet push,
155+
/// this is translated to 5ms difference sampling. E.g. 1st sample 0ms, 2nd 5ms,
156+
/// 3rd 10ms. Using all 3 samples let you have a 5ms precision instead of 15ms.
157+
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
158+
#[packed_struct(bit_numbering = "msb0", size_bytes = "12")]
159+
pub struct ImuData {
160+
#[packed_field(bytes = "0..=1", endian = "lsb")]
161+
pub accel_x: Integer<i16, packed_bits::Bits<16>>,
162+
#[packed_field(bytes = "2..=3", endian = "lsb")]
163+
pub accel_y: Integer<i16, packed_bits::Bits<16>>,
164+
#[packed_field(bytes = "4..=5", endian = "lsb")]
165+
pub accel_z: Integer<i16, packed_bits::Bits<16>>,
166+
#[packed_field(bytes = "6..=7", endian = "lsb")]
167+
pub gyro_x: Integer<i16, packed_bits::Bits<16>>,
168+
#[packed_field(bytes = "8..=9", endian = "lsb")]
169+
pub gyro_y: Integer<i16, packed_bits::Bits<16>>,
170+
#[packed_field(bytes = "10..=11", endian = "lsb")]
171+
pub gyro_z: Integer<i16, packed_bits::Bits<16>>,
172+
}
173+
174+
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
175+
#[packed_struct(bit_numbering = "msb0", size_bytes = "64")]
176+
pub struct PackedInputDataReport {
177+
// byte 0-2
178+
/// Input report ID
179+
#[packed_field(bytes = "0", ty = "enum")]
180+
pub id: ReportType,
181+
/// Timer. Increments very fast. Can be used to estimate excess Bluetooth latency.
182+
#[packed_field(bytes = "1")]
183+
pub timer: u8,
184+
/// Battery and connection information
185+
#[packed_field(bytes = "2")]
186+
pub info: BatteryConnection,
187+
188+
// byte 3-5
189+
/// Button status
190+
#[packed_field(bytes = "3..=5")]
191+
pub buttons: ButtonStatus,
192+
193+
// byte 6-11
194+
/// Left analog stick
195+
#[packed_field(bytes = "6..=8")]
196+
pub left_stick: StickData,
197+
/// Right analog stick
198+
#[packed_field(bytes = "9..=11")]
199+
pub right_stick: StickData,
200+
201+
// byte 12
202+
/// Vibrator input report. Decides if next vibration pattern should be sent.
203+
#[packed_field(bytes = "12")]
204+
pub vibrator_report: u8,
205+
}

src/drivers/switch/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod driver;
2+
pub mod event;
3+
pub mod hid_report;
4+
pub mod report_descriptor;

src/drivers/switch/report_descriptor.rs

Whitespace-only changes.

0 commit comments

Comments
 (0)