-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Device): Add support for left/right touchpad events for the Oran…
…gePi Neo.
- Loading branch information
Showing
12 changed files
with
538 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
pub mod dualsense; | ||
pub mod iio_imu; | ||
pub mod lego; | ||
pub mod opineo; | ||
pub mod steam_deck; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
use std::{ | ||
error::Error, | ||
ffi::CString, | ||
time::{Duration, Instant}, | ||
u8, vec, | ||
}; | ||
|
||
use hidapi::HidDevice; | ||
use packed_struct::{types::SizedInteger, PackedStruct}; | ||
|
||
use super::{ | ||
event::{Event, TouchAxisInput}, | ||
hid_report::TouchpadDataReport, | ||
}; | ||
|
||
// Hardware ID's | ||
pub const VID: u16 = 0x0911; | ||
pub const PID: u16 = 0x5288; | ||
// Report ID | ||
pub const TOUCH_DATA: u8 = 0x04; | ||
// Input report size | ||
const TOUCHPAD_PACKET_SIZE: usize = 10; | ||
// HID buffer read timeout | ||
const HID_TIMEOUT: i32 = 10; | ||
// Input report axis ranges | ||
pub const PAD_X_MAX: f64 = 512.0; | ||
pub const PAD_Y_MAX: f64 = 512.0; | ||
|
||
pub struct Driver { | ||
/// HIDRAW device instance | ||
device: HidDevice, | ||
/// Whether or not we are detecting a touch event currently. | ||
is_touching: bool, | ||
/// Timestamp of the last touch event. | ||
last_touch: Instant, | ||
/// State for the touchpad device | ||
touchpad_state: Option<TouchpadDataReport>, | ||
} | ||
|
||
impl Driver { | ||
pub fn new(path: String) -> Result<Self, Box<dyn Error + Send + Sync>> { | ||
let fmtpath = path.clone(); | ||
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(format!("Device '{fmtpath}' is not a OrangePi NEO Controller").into()); | ||
} | ||
|
||
Ok(Self { | ||
device, | ||
is_touching: false, | ||
last_touch: Instant::now(), | ||
touchpad_state: None, | ||
}) | ||
} | ||
|
||
/// Poll the device and read input reports | ||
pub fn poll(&mut self) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> { | ||
// Read data from the device into a buffer | ||
let mut buf = [0; TOUCHPAD_PACKET_SIZE]; | ||
let bytes_read = self.device.read_timeout(&mut buf[..], HID_TIMEOUT)?; | ||
|
||
let report_id = buf[0]; | ||
let slice = &buf[..bytes_read]; | ||
//log::trace!("Got Report ID: {report_id}"); | ||
//log::trace!("Got Report Size: {bytes_read}"); | ||
|
||
let mut events = match report_id { | ||
TOUCH_DATA => { | ||
log::trace!("Got touch data."); | ||
if bytes_read != TOUCHPAD_PACKET_SIZE { | ||
return Err("Invalid packet size for Keyboard or Touchpad Data.".into()); | ||
} | ||
// Handle the incoming input report | ||
let sized_buf = slice.try_into()?; | ||
|
||
self.handle_touchinput_report(sized_buf)? | ||
} | ||
_ => { | ||
//log::trace!("Invalid Report ID."); | ||
let events = vec![]; | ||
events | ||
} | ||
}; | ||
|
||
if self.is_touching && (self.last_touch.elapsed() > Duration::from_millis(4)) { | ||
let event: Event = self.release_touch(); | ||
events.push(event); | ||
} | ||
|
||
Ok(events) | ||
} | ||
|
||
/// Unpacks the buffer into a [TouchpadDataReport] structure and updates | ||
/// the internal touchpad_state | ||
fn handle_touchinput_report( | ||
&mut self, | ||
buf: [u8; TOUCHPAD_PACKET_SIZE], | ||
) -> Result<Vec<Event>, Box<dyn Error + Send + Sync>> { | ||
let input_report = TouchpadDataReport::unpack(&buf)?; | ||
|
||
// Print input report for debugging | ||
//log::trace!("--- Input report ---"); | ||
//log::trace!("{input_report}"); | ||
//log::trace!("---- End Report ----"); | ||
|
||
// Update the state | ||
let old_dinput_state = self.update_touchpad_state(input_report); | ||
|
||
// Translate the state into a stream of input events | ||
let events = self.translate_touch(old_dinput_state); | ||
|
||
Ok(events) | ||
} | ||
|
||
/// Update touchinput state | ||
fn update_touchpad_state( | ||
&mut self, | ||
input_report: TouchpadDataReport, | ||
) -> Option<TouchpadDataReport> { | ||
let old_state = self.touchpad_state; | ||
self.touchpad_state = Some(input_report); | ||
old_state | ||
} | ||
|
||
/// Translate the state into individual events | ||
fn translate_touch(&mut self, old_state: Option<TouchpadDataReport>) -> Vec<Event> { | ||
let mut events = Vec::new(); | ||
let Some(state) = self.touchpad_state else { | ||
return events; | ||
}; | ||
|
||
// Translate state changes into events if they have changed | ||
let Some(_) = old_state else { | ||
return events; | ||
}; | ||
//// Axis events | ||
if !self.is_touching { | ||
self.is_touching = true; | ||
log::trace!("Started TOUCH event"); | ||
} | ||
events.push(Event::TouchAxis(TouchAxisInput { | ||
index: 0, | ||
is_touching: true, | ||
x: state.touch_x.to_primitive(), | ||
y: state.touch_y.to_primitive(), | ||
})); | ||
|
||
self.last_touch = Instant::now(); | ||
events | ||
} | ||
|
||
fn release_touch(&mut self) -> Event { | ||
log::trace!("Released TOUCH event."); | ||
self.is_touching = false; | ||
Event::TouchAxis(TouchAxisInput { | ||
index: 0, | ||
is_touching: false, | ||
x: 0, | ||
y: 0, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/// Events that can be emitted by the Steam Deck controller | ||
#[derive(Clone, Debug)] | ||
pub enum Event { | ||
TouchAxis(TouchAxisInput), | ||
} | ||
|
||
/// Axis input contain (x, y) coordinates | ||
#[derive(Clone, Debug)] | ||
pub struct TouchAxisInput { | ||
pub index: u8, | ||
pub is_touching: bool, | ||
pub x: u16, | ||
pub y: u16, | ||
} | ||
|
||
/// TouchAxisID tracks the sequential count of touch inputs | ||
#[derive(Clone, Debug)] | ||
pub struct TouchAxisID { | ||
pub value: u32, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
//! Reference: https://gitlab.com/open-sd/opensd/-/blob/main/src/opensdd/drivers/gamepad/hid_reports.hpp | ||
#![allow(warnings)] | ||
use packed_struct::prelude::*; | ||
|
||
/// Different report types | ||
pub enum ReportType { | ||
TouchpadData = 0x04, | ||
} | ||
|
||
impl ReportType { | ||
pub fn to_u8(&self) -> u8 { | ||
match self { | ||
ReportType::TouchpadData => ReportType::TouchpadData as u8, | ||
} | ||
} | ||
} | ||
|
||
// TouchpadData | ||
// | ||
// # ReportID: 4 / Confidence: 1 | Tip Switch: 0 | Contact Id: 0 | # | X: 0 | Y: 0 | Scan Time: 40742 | Contact Count: 1 | Button: 0 0 0 | # | ||
// E: 000000.000000 10 04 01 00 00 00 00 26 9f 01 00 | ||
// | ||
// X Axis | ||
// # ReportID: 4 / Confidence: 1 | Tip Switch: 0 | Contact Id: 0 | # | X: 512 | Y: 0 | Scan Time: 49251 | Contact Count: 1 | Button: 0 0 0 | # | ||
// E: 000022.851877 10 04 01 00 02 00 00 63 c0 01 00 | ||
// # ReportID: 4 / Confidence: 1 | Tip Switch: 0 | Contact Id: 0 | # | X: 467 | Y: 0 | Scan Time: 2804 | Contact Count: 1 | Button: 0 0 0 | # | ||
// E: 000455.355131 10 04 01 d3 01 00 00 f4 0a 01 00 | ||
// | ||
// Y Axis | ||
// # ReportID: 4 / Confidence: 1 | Tip Switch: 0 | Contact Id: 0 | # | X: 0 | Y: 512 | Scan Time: 11269 | Contact Count: 1 | Button: 0 0 0 | # | ||
//E: 000054.609703 10 04 01 00 00 00 02 05 2c 01 00 | ||
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)] | ||
#[packed_struct(bit_numbering = "msb0", size_bytes = "10")] | ||
pub struct TouchpadDataReport { | ||
// BYTE 0 | ||
#[packed_field(bytes = "0")] | ||
pub report_id: u8, | ||
// BYTE 1 | ||
#[packed_field(bytes = "1")] | ||
pub confidence: u8, | ||
// BYTE 2-3 | ||
#[packed_field(bytes = "2..=3", endian = "lsb")] | ||
pub touch_x: Integer<u16, packed_bits::Bits<16>>, | ||
// BYTE 4 | ||
#[packed_field(bytes = "4")] | ||
pub unk_4: u8, | ||
// BYTE 5-6 | ||
#[packed_field(bytes = "5..=6", endian = "lsb")] | ||
pub touch_y: Integer<u16, packed_bits::Bits<16>>, | ||
// BYTE 7-8 | ||
#[packed_field(bytes = "7..=8", endian = "lsb")] | ||
pub scan_time: Integer<u16, packed_bits::Bits<16>>, | ||
// BYTE 9 | ||
#[packed_field(bytes = "9")] | ||
pub unk_9: u8, | ||
} | ||
|
||
impl Default for TouchpadDataReport { | ||
fn default() -> Self { | ||
Self { | ||
report_id: Default::default(), | ||
confidence: Default::default(), | ||
touch_x: Default::default(), | ||
unk_4: Default::default(), | ||
touch_y: Default::default(), | ||
scan_time: Default::default(), | ||
unk_9: Default::default(), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
use std::error::Error; | ||
|
||
use packed_struct::types::{Integer, SizedInteger}; | ||
|
||
use crate::drivers::opineo::hid_report::TouchpadDataReport; | ||
|
||
#[tokio::test] | ||
async fn test_opi_hid() -> Result<(), Box<dyn Error>> { | ||
let mut report = TouchpadDataReport::default(); | ||
println!("Before Report: {}", report); | ||
report.touch_x = Integer::from_primitive(467); | ||
report.touch_y = Integer::from_primitive(512); | ||
|
||
println!("After Report: {}", report); | ||
assert_eq!(report.touch_x.to_primitive(), 467); | ||
assert_eq!(report.touch_y.to_primitive(), 512); | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pub mod driver; | ||
pub mod event; | ||
pub mod hid_report; | ||
#[cfg(test)] | ||
pub mod hid_report_test; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.