Skip to content
Merged
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
7 changes: 6 additions & 1 deletion crates/hypercolor-core/src/device/blocks/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,12 @@ fn touch_edge(touch: &BlocksTouch) -> DeviceInputEdge {
pressure: touch.z,
};
match touch.action {
BlocksTouchAction::Start => DeviceInputEdge::TouchBegan { contact, position },
// A block has one touch surface, so its touches name no segment.
BlocksTouchAction::Start => DeviceInputEdge::TouchBegan {
contact,
position,
segment: None,
},
BlocksTouchAction::Move => DeviceInputEdge::TouchMoved { contact, position },
BlocksTouchAction::End => DeviceInputEdge::TouchEnded { contact, position },
}
Expand Down
1 change: 1 addition & 0 deletions crates/hypercolor-core/src/device/blocks/input/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ async fn an_aborted_stream_cannot_publish_into_a_reconnected_device() {
y: 0.5,
pressure: 0.25,
},
segment: None,
},
}),
"the replacement stream still publishes"
Expand Down
69 changes: 65 additions & 4 deletions crates/hypercolor-core/src/device/usb_backend.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! USB backend that bridges HAL protocols to the core `DeviceBackend` trait.

mod actor;
mod input;

use std::cmp::min;
use std::collections::HashMap;
Expand All @@ -12,8 +13,8 @@ use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail};
use hypercolor_driver_api::{
BackendInfo, ConnectExecution, DeviceBackend, DeviceDeliveryAck, DeviceDeliveryId,
DeviceDeliveryObserver, DeviceDisplaySink, DeviceFrameSink, DeviceLifecyclePolicy,
DiscoveredDevice,
DeviceDeliveryObserver, DeviceDisplaySink, DeviceFrameSink, DeviceInputSink,
DeviceLifecyclePolicy, DiscoveredDevice,
};
use hypercolor_hal::database::{DeviceDescriptor, TransportType};
use hypercolor_hal::protocol::Protocol;
Expand All @@ -27,6 +28,7 @@ use hypercolor_hal::transport::bulk::UsbBulkTransport;
use hypercolor_hal::transport::control::UsbControlTransport;
use hypercolor_hal::transport::hid::UsbHidTransport;
use hypercolor_hal::transport::hidapi::UsbHidApiTransport;
use hypercolor_hal::transport::input::TransportInputSink;
use hypercolor_hal::transport::serial::UsbSerialTransport;
use hypercolor_hal::transport::vendor::UsbVendorTransport;
use hypercolor_hal::transport::{HidRawOpenRequest, Transport, TransportError};
Expand All @@ -38,6 +40,7 @@ use tokio::sync::{RwLock, mpsc, oneshot, watch};
use tokio::task::JoinHandle;
use tracing::{debug, info, trace};

use self::input::UsbDeviceInput;
use super::transport_error::{DeviceTransportOperation, map_hal_transport_error};
use crate::attachment::ComponentRegistry;

Expand Down Expand Up @@ -356,6 +359,8 @@ struct UsbDevice {
info_template: DeviceInfo,
frame_diagnostics_emitted: bool,
non_black_frame_diagnostics_emitted: bool,
/// Input the transport decodes, published while the device is connected.
input: Option<Arc<UsbDeviceInput>>,
}

impl UsbDevice {
Expand Down Expand Up @@ -469,6 +474,11 @@ impl UsbDevice {
}

async fn shutdown(&mut self, device_id: DeviceId) -> std::result::Result<(), DeviceError> {
// Input stops counting the moment the device starts going away, so
// held touches cancel now rather than after the shutdown sequence.
if let Some(input) = &self.input {
input.close();
}
{
let _gate = lock_lifecycle_gate(&self.lifecycle_gate);
self.active.store(false, Ordering::Release);
Expand Down Expand Up @@ -583,6 +593,9 @@ impl UsbDevice {

impl Drop for UsbDevice {
fn drop(&mut self) {
if let Some(input) = &self.input {
input.close();
}
let _gate = lock_lifecycle_gate(&self.lifecycle_gate);
self.active.store(false, Ordering::Release);
let device_id = self.info_template.id;
Expand Down Expand Up @@ -863,6 +876,8 @@ pub struct UsbBackend {
pending: StdRwLock<HashMap<DeviceId, PendingUsbDevice>>,
connected: StdRwLock<HashMap<DeviceId, Arc<ConnectedUsbDevice>>>,
protocol_configs: UsbProtocolConfigStore,
/// Host sink for input from transports that decode it.
device_input: Option<Arc<dyn DeviceInputSink>>,
}

struct ConnectedUsbDevice {
Expand All @@ -889,6 +904,37 @@ impl UsbBackend {
}
}

/// Publish input from connected devices whose transport decodes it.
///
/// Each such device publishes under a lease attached with its canonical
/// `DeviceId` once connect succeeds and retired at disconnect.
#[must_use]
pub fn with_device_input(mut self, sink: Arc<dyn DeviceInputSink>) -> Self {
self.device_input = Some(sink);
self
}

/// Input bridge for a device whose transport decodes input, when the
/// host accepts device input.
fn device_input_for(
&self,
pending: &PendingUsbDevice,
device_id: DeviceId,
) -> Option<Arc<UsbDeviceInput>> {
let TransportType::DriverUsb { binding } = pending.descriptor.transport else {
return None;
};
if !binding.publishes_input {
return None;
}
let sink = Arc::clone(self.device_input.as_ref()?);
Some(Arc::new(UsbDeviceInput::new(
sink,
device_id,
&pending.info_template.name,
)))
}

async fn build_protocol(
&self,
pending: &PendingUsbDevice,
Expand All @@ -907,6 +953,7 @@ impl UsbBackend {
async fn build_transport(
pending: &PendingUsbDevice,
usb: &nusb::DeviceInfo,
input: Option<Arc<dyn TransportInputSink>>,
) -> Result<Box<dyn Transport>> {
match pending.descriptor.transport {
TransportType::UsbHidApi {
Expand Down Expand Up @@ -984,7 +1031,7 @@ impl UsbBackend {
report_id,
} => Self::open_bulk_transport(pending, usb, interface, report_id).await,
TransportType::DriverUsb { binding } => {
Self::open_driver_usb_transport(pending, usb, binding).await
Self::open_driver_usb_transport(pending, usb, binding, input).await
}
TransportType::UsbSerial { baud_rate } => {
Self::open_serial_transport(pending, baud_rate)
Expand Down Expand Up @@ -1051,6 +1098,7 @@ impl UsbBackend {
pending: &PendingUsbDevice,
usb: &nusb::DeviceInfo,
binding: UsbTransportBinding,
input: Option<Arc<dyn TransportInputSink>>,
) -> Result<Box<dyn Transport>> {
let device = Self::open_usb_device(pending, usb).await?;
(binding.open)(UsbTransportOpenRequest {
Expand All @@ -1059,6 +1107,7 @@ impl UsbBackend {
product_id: pending.product_id,
serial: pending.serial.clone(),
usb_path: pending.usb_path.clone(),
input,
})
.await
.with_context(|| {
Expand Down Expand Up @@ -1281,7 +1330,12 @@ impl DeviceBackend for UsbBackend {
})?;

let protocol: Arc<dyn Protocol> = Arc::from(self.build_protocol(&pending, *id).await);
let transport: Arc<dyn Transport> = Arc::from(Self::build_transport(&pending, &usb).await?);
let input = self.device_input_for(&pending, *id);
let transport_input = input
.clone()
.map(|input| input as Arc<dyn TransportInputSink>);
let transport: Arc<dyn Transport> =
Arc::from(Self::build_transport(&pending, &usb, transport_input).await?);
let init_sequence = protocol.init_sequence();
let first_init_packet = init_sequence.first().map_or_else(
|| "<none>".to_owned(),
Expand Down Expand Up @@ -1403,6 +1457,7 @@ impl DeviceBackend for UsbBackend {
Arc::clone(&last_async_error),
);

let connected_input = input.clone();
let device = UsbDevice {
protocol,
transport_name,
Expand All @@ -1419,6 +1474,7 @@ impl DeviceBackend for UsbBackend {
info_template: pending.info_template,
frame_diagnostics_emitted: false,
non_black_frame_diagnostics_emitted: false,
input,
};
let frame_sink = device.frame_sink(*id);
let display_sink = device
Expand All @@ -1437,6 +1493,11 @@ impl DeviceBackend for UsbBackend {
.write()
.unwrap_or_else(PoisonError::into_inner)
.insert(*id, Arc::new(connected));
// Input counts only once the device is fully connected, so a connect
// that fails partway never shows up as an input source.
if let Some(input) = connected_input {
input.activate();
}

Ok(())
}
Expand Down
97 changes: 97 additions & 0 deletions crates/hypercolor-core/src/device/usb_backend/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! Input from driver-owned USB transports, published under the host's lease.
//!
//! A transport that decodes device input gets one [`UsbDeviceInput`] when it
//! opens. The backend attaches the host lease only once connect succeeds, so
//! a device that never finishes connecting never shows up as an input
//! source, and retires it at disconnect. Between those points the transport
//! decides when held input can no longer be trusted: an interruption
//! supersedes the lease, and closing input retires it, so the host cancels
//! every hold whose lift will never be seen.

use std::sync::{Arc, Mutex, MutexGuard, PoisonError};

use hypercolor_driver_api::{DeviceInputPublisher, DeviceInputSink};
use hypercolor_hal::transport::input::TransportInputSink;
use hypercolor_types::device::DeviceId;
use hypercolor_types::device_input::DeviceInputEdge;

/// Bridge from one transport's decoded input to the host's device lease.
pub(super) struct UsbDeviceInput {
sink: Arc<dyn DeviceInputSink>,
device_id: DeviceId,
label: Arc<str>,
lease: Mutex<Lease>,
}

enum Lease {
/// Connect has not finished, so input is dropped.
Pending,
Live(Box<dyn DeviceInputPublisher>),
/// Input ended or the device disconnected; nothing attaches again.
Closed,
}

impl UsbDeviceInput {
pub(super) fn new(sink: Arc<dyn DeviceInputSink>, device_id: DeviceId, label: &str) -> Self {
Self {
sink,
device_id,
label: Arc::from(label),
lease: Mutex::new(Lease::Pending),
}
}

/// Start publishing under a fresh lease once the device is connected.
///
/// Does nothing after [`Self::close`], so a transport whose input
/// already ended cannot bring the lease back.
pub(super) fn activate(&self) {
let mut lease = self.lock();
if matches!(*lease, Lease::Pending) {
*lease = Lease::Live(self.sink.attach(self.device_id, &self.label));
}
}

/// Retire the lease for good, cancelling whatever the device held.
pub(super) fn close(&self) {
let retired = std::mem::replace(&mut *self.lock(), Lease::Closed);
drop(retired);
}

fn lock(&self) -> MutexGuard<'_, Lease> {
self.lease.lock().unwrap_or_else(PoisonError::into_inner)
}
}

impl TransportInputSink for UsbDeviceInput {
fn publish(&self, edges: &[DeviceInputEdge]) {
let mut lease = self.lock();
if let Lease::Live(publisher) = &*lease
&& !publisher.publish(edges)
{
// A newer connection of the same device owns its input now.
*lease = Lease::Closed;
}
}

fn interrupted(&self) {
let mut lease = self.lock();
let Lease::Live(publisher) = &mut *lease else {
return;
};
// Renewing cancels this lease's holds and replaces it in one step on
// the host, and only while it is still current, so a lease a newer
// connection superseded can never take the device back.
match publisher.renew() {
Some(renewed) => *publisher = renewed,
None => *lease = Lease::Closed,
}
}

fn closed(&self) {
self.close();
}
}

#[cfg(test)]
mod tests;
Loading
Loading