Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 85 additions & 38 deletions src/drivers/opineo/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use std::{
use hidapi::HidDevice;
use packed_struct::{types::SizedInteger, PackedStruct};

use crate::udev::device::UdevDevice;
use crate::{
drivers::opineo::event::{TriggerEvent, TriggerInput},
udev::device::UdevDevice,
};

use super::{
event::{BinaryInput, Event, TouchAxisEvent, TouchButtonEvent},
Expand All @@ -32,18 +35,24 @@ 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 const PAD_FORCE_MAX: f64 = 127.0;
pub const PAD_FORCE_NORMAL: u8 = 32; /* Simulated average */

const CLICK_DELAY: Duration = Duration::from_millis(75);

pub struct Driver {
/// HIDRAW device instance
device: HidDevice,
/// Timestamp of the first touch event.
first_touch: Instant,
/// Whether or not we are currently holding a click-to-click.
is_clicked: bool,
/// Whether or not we are detecting a touch event currently.
is_touching: bool,
/// Whether or not we are currently holding a tap-to-click.
is_tapped: bool,
/// Timestamp of the last touch event. Used to track if the touch has ended.
/// Timestamp of the last touch event.
last_touch: Instant,
/// Timestamp of the first touch event. Used to detect tap-to-click events
first_touch: Instant,
/// Whether or not a touch event was started that hasn't been cleared.
touch_started: bool,
/// State for the touchpad device
touchpad_state: Option<TouchpadDataReport>,
}
Expand All @@ -62,9 +71,10 @@ impl Driver {
Ok(Self {
device,
first_touch: Instant::now(),
is_tapped: false,
is_clicked: false,
is_touching: false,
last_touch: Instant::now(),
touch_started: false,
touchpad_state: None,
})
}
Expand All @@ -77,8 +87,6 @@ impl Driver {

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 => {
Expand All @@ -92,35 +100,43 @@ impl Driver {
self.handle_touchinput_report(sized_buf)?
}
_ => {
//log::trace!("Invalid Report ID.");
let events = vec![];
events
}
};

// There is no release event, so check to see if we are still touching.
if self.is_touching && (self.last_touch.elapsed() > Duration::from_millis(4)) {
let event: Event = self.release_touch();
events.push(event);
// Check for tap events
if self.first_touch.elapsed() < Duration::from_millis(200) {
// For double clicking, ensure the previous tap is cleared.
if self.is_tapped {
let event: Event = self.release_tap();
events.push(event);
}
let event: Event = self.start_tap();
events.push(event);
if self.is_touching {
if self.last_touch.elapsed() >= Duration::from_millis(4) {
self.is_touching = false;
}
return Ok(events);
}

// Check for quick click conditions
if self.touch_started && self.first_touch.elapsed() <= CLICK_DELAY * 2 {
// For double clicking, ensure the previous click is cleared.
if self.is_clicked {
let mut new_events = self.release_click();
events.append(&mut new_events);
}

let mut new_events = self.start_click();
events.append(&mut new_events);

return Ok(events);
}

// If we did a click event, see if we shoudl release it. Accounts for click and drag.
if !self.is_touching
&& self.is_tapped
&& (self.last_touch.elapsed() > Duration::from_millis(100))
{
let event: Event = self.release_tap();
// Check for release conditions
if self.touch_started && self.last_touch.elapsed() > CLICK_DELAY / 2 {
let event: Event = self.release_touch();
events.push(event);

// If we did a click event, see if we should release it. Accounts for click and drag.
if self.is_clicked {
let mut new_events = self.release_click();
events.append(&mut new_events);
return Ok(events);
}
}

Ok(events)
Expand Down Expand Up @@ -172,8 +188,16 @@ impl Driver {
//// Axis events
if !self.is_touching {
self.is_touching = true;
self.first_touch = Instant::now();
log::trace!("Started TOUCH event");
// Check for click events
if self.touch_started && self.last_touch.elapsed() <= CLICK_DELAY / 3 {
let mut new_events = self.start_click();
events.append(&mut new_events);
}
if !self.touch_started {
self.touch_started = true;
self.first_touch = Instant::now();
log::trace!("Started TOUCH event");
}
}
events.push(Event::TouchAxis(TouchAxisEvent {
index: 0,
Expand All @@ -188,7 +212,7 @@ impl Driver {

fn release_touch(&mut self) -> Event {
log::trace!("Released TOUCH event.");
self.is_touching = false;
self.touch_started = false;
Event::TouchAxis(TouchAxisEvent {
index: 0,
is_touching: false,
Expand All @@ -197,15 +221,38 @@ impl Driver {
})
}

fn start_tap(&mut self) -> Event {
fn start_click(&mut self) -> Vec<Event> {
log::trace!("Started CLICK event.");
self.is_tapped = true;
Event::TouchButton(TouchButtonEvent::Left(BinaryInput { pressed: true }))
log::trace!("First touch elapsed: {:?}", self.first_touch.elapsed());
log::trace!("Last touch elapsed: {:?}", self.last_touch.elapsed());
self.is_clicked = true;
let mut events = Vec::new();

let event = Event::TouchButton(TouchButtonEvent::Left(BinaryInput { pressed: true }));
events.push(event);
// The touchpad doesn't have a force sensor. The deck target wont produce a "click"
// event in desktop or lizard mode without a force value. Simulate a 1/4 press to work
// around this.
let event = Event::Trigger(TriggerEvent::PadForce(TriggerInput {
value: PAD_FORCE_NORMAL,
}));
events.push(event);
events
}

fn release_tap(&mut self) -> Event {
fn release_click(&mut self) -> Vec<Event> {
log::trace!("Released CLICK event.");
self.is_tapped = false;
Event::TouchButton(TouchButtonEvent::Left(BinaryInput { pressed: false }))
log::trace!("First touch elapsed: {:?}", self.first_touch.elapsed());
log::trace!("Last touch elapsed: {:?}", self.last_touch.elapsed());
self.is_clicked = false;
let mut events = Vec::new();
let event = Event::TouchButton(TouchButtonEvent::Left(BinaryInput { pressed: false }));
events.push(event);
// The touchpad doesn't have a force sensor. The deck target wont produce a "click"
// event in desktop or lizard mode without a force value. Simulate a 1/4 press to work
// around this.
let event = Event::Trigger(TriggerEvent::PadForce(TriggerInput { value: 0 }));
events.push(event);
Comment on lines +251 to +255
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of emulating the touch force in the source device, can we instead handle this in the target device(s)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not manage source device edge cases in target devices.

events
}
}
13 changes: 13 additions & 0 deletions src/drivers/opineo/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pub enum Event {
TouchAxis(TouchAxisEvent),
TouchButton(TouchButtonEvent),
Trigger(TriggerEvent),
}

/// Axis input contain (x, y) coordinates
Expand All @@ -20,9 +21,21 @@ pub struct BinaryInput {
pub pressed: bool,
}

/// Trigger input contains non-negative integers
#[derive(Clone, Debug)]
pub struct TriggerInput {
pub value: u8,
}

/// Button events represend binary inputs
#[derive(Clone, Debug)]
pub enum TouchButtonEvent {
/// Tap to click button
Left(BinaryInput),
}

/// Trigger events contain values indicating how far a trigger is pulled
#[derive(Clone, Debug)]
pub enum TriggerEvent {
PadForce(TriggerInput),
}
43 changes: 33 additions & 10 deletions src/input/source/hidraw/opineo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ use std::{error::Error, fmt::Debug};

use crate::{
drivers::opineo::{
driver::{self, Driver, LPAD_NAMES, RPAD_NAMES},
driver::{self, Driver, LPAD_NAMES, PAD_FORCE_MAX, RPAD_NAMES},
event,
},
input::{
capability::{Capability, Touch, TouchButton, Touchpad},
capability::{Capability, Gamepad, GamepadTrigger, Touch, TouchButton, Touchpad},
event::{native::NativeEvent, value::InputValue},
output_capability::{OutputCapability, LED},
output_capability::OutputCapability,
source::{InputError, OutputError, SourceInputDevice, SourceOutputDevice},
},
udev::device::UdevDevice,
Expand Down Expand Up @@ -120,6 +120,16 @@ fn normalize_axis_value(event: event::TouchAxisEvent) -> InputValue {
}
}

/// Normalize the trigger value to something between 0.0 and 1.0 based on the
/// Orange Pi's maximum axis ranges.
fn normalize_trigger_value(event: event::TriggerEvent) -> InputValue {
match event {
event::TriggerEvent::PadForce(value) => {
InputValue::Float(normalize_unsigned_value(value.value as f64, PAD_FORCE_MAX))
}
}
}

/// Translate the given OrangePi NEO events into native events
fn translate_events(events: Vec<event::Event>, touchpad_side: TouchpadSide) -> Vec<NativeEvent> {
let mut translated = Vec::with_capacity(events.len());
Expand All @@ -145,8 +155,6 @@ fn translate_event(event: event::Event, touchpad_side: TouchpadSide) -> NativeEv
normalize_axis_value(axis),
),
},
// TODO: Consider making a [TouchButton::Tap] event so we can do more events with touchpads
// that have physical buttons (e.g. Steam Deck).
event::Event::TouchButton(button) => match button {
event::TouchButtonEvent::Left(value) => match touchpad_side {
TouchpadSide::Unknown => {
Expand All @@ -162,18 +170,33 @@ fn translate_event(event: event::Event, touchpad_side: TouchpadSide) -> NativeEv
),
},
},
event::Event::Trigger(trigg) => match trigg.clone() {
event::TriggerEvent::PadForce(_) => match touchpad_side {
TouchpadSide::Unknown => {
NativeEvent::new(Capability::NotImplemented, InputValue::Bool(false))
}
TouchpadSide::Left => NativeEvent::new(
Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTouchpadForce)),
normalize_trigger_value(trigg),
),
TouchpadSide::Right => NativeEvent::new(
Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTouchpadForce)),
normalize_trigger_value(trigg),
),
},
},
}
}

/// List of all capabilities that the OrangePi NEO driver implements
/// List of all input capabilities that the OrangePi NEO driver implements
pub const CAPABILITIES: &[Capability] = &[
Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTouchpadForce)),
Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTouchpadForce)),
Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Press))),
Capability::Touchpad(Touchpad::LeftPad(Touch::Motion)),
Capability::Touchpad(Touchpad::RightPad(Touch::Button(TouchButton::Press))),
Capability::Touchpad(Touchpad::RightPad(Touch::Motion)),
];

pub const OUTPUT_CAPABILITIES: &[OutputCapability] = &[
OutputCapability::ForceFeedback,
OutputCapability::LED(LED::Color),
];
/// List of all output capabilities that the OrangePi NEO supports
pub const OUTPUT_CAPABILITIES: &[OutputCapability] = &[OutputCapability::ForceFeedback];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the OrangePi Neo not support LEDs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has RGB that is only controlled through button combo's on the device. OrangePi did not want to spend the money for an API for the MCU.