diff --git a/crates/android-usb-serial/CHANGELOG.md b/crates/android-usb-serial/CHANGELOG.md index 330d565a..6c8f9bcf 100644 --- a/crates/android-usb-serial/CHANGELOG.md +++ b/crates/android-usb-serial/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- Track CDC ACM interrupt-IN notifications and expose DCD, DSR, and ring state + through the modem-status API + ## 0.1.0 - Initial release: FTDI, CP21xx, CH34x, Prolific, CDC-ACM, GSM modem, Chrome CCD drivers on `nusb` diff --git a/crates/android-usb-serial/src/drivers/cdc_acm.rs b/crates/android-usb-serial/src/drivers/cdc_acm.rs index e2f539e0..db2f6ca6 100644 --- a/crates/android-usb-serial/src/drivers/cdc_acm.rs +++ b/crates/android-usb-serial/src/drivers/cdc_acm.rs @@ -2,14 +2,135 @@ use super::{line_coding_bytes, Driver, EndpointPair, ModemStatus, WRITE_TIMEOUT_MS}; use crate::config::{FlowControl, LineConfig, PurgeKind}; -use crate::error::{Result, UsbSerialError}; +use crate::error::{ReadOutcome, Result, UsbSerialError}; use crate::reader::SerialReader; -use crate::transport::{ControlRequest, SharedTransport, USB_RECIP_INTERFACE, USB_TYPE_CLASS}; +use crate::transport::{ + BulkIn, ControlRequest, SharedTransport, USB_RECIP_INTERFACE, USB_TYPE_CLASS, +}; +use std::sync::atomic::{AtomicBool, AtomicU16, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread::{self, JoinHandle}; const USB_SUBCLASS_ACM: u8 = 2; const SET_LINE_CODING: u8 = 0x20; const SET_CONTROL_LINE_STATE: u8 = 0x22; const SEND_BREAK: u8 = 0x23; +const NOTIFICATION_REQUEST_TYPE: u8 = 0xa1; +const SERIAL_STATE_NOTIFICATION: u8 = 0x20; +const SERIAL_STATE_NOTIFICATION_SIZE: usize = 10; +const SERIAL_STATE_RX_CARRIER: u16 = 1 << 0; +const SERIAL_STATE_TX_CARRIER: u16 = 1 << 1; +const SERIAL_STATE_RING_SIGNAL: u16 = 1 << 3; +const NOTIFICATION_READ_TIMEOUT_MS: u32 = 200; + +struct CdcNotificationReader { + state: Arc, + error: Arc>>, + stop: Arc, + thread: Option>, +} + +impl CdcNotificationReader { + fn start(mut endpoint: Box, max_packet_size: u16) -> Self { + let state = Arc::new(AtomicU16::new(0)); + let error = Arc::new(Mutex::new(None)); + let stop = Arc::new(AtomicBool::new(false)); + let thread_state = Arc::clone(&state); + let thread_error = Arc::clone(&error); + let thread_stop = Arc::clone(&stop); + let buffer_size = usize::from(max_packet_size).max(SERIAL_STATE_NOTIFICATION_SIZE); + + let thread = thread::spawn(move || { + let mut buffer = vec![0; buffer_size]; + while !thread_stop.load(Ordering::Relaxed) { + match endpoint.read(&mut buffer, NOTIFICATION_READ_TIMEOUT_MS) { + Ok(ReadOutcome::Data(data)) if !data.is_empty() => { + match parse_serial_state_notification(&data) { + Ok(Some(serial_state)) => { + thread_state.store(serial_state, Ordering::Relaxed); + } + Ok(None) => {} + Err(error) => { + *thread_error.lock().unwrap() = Some(error); + break; + } + } + } + Ok(ReadOutcome::TimedOut) | Ok(ReadOutcome::Data(_)) => {} + Ok(ReadOutcome::Cancelled) => break, + Err(error) => { + if !thread_stop.load(Ordering::Relaxed) { + *thread_error.lock().unwrap() = Some(error.to_string()); + } + break; + } + } + } + }); + + Self { + state, + error, + stop, + thread: Some(thread), + } + } + + fn modem_status(&self) -> Result { + if let Some(error) = self.error.lock().unwrap().take() { + return Err(UsbSerialError::Io(error)); + } + + let state = self.state.load(Ordering::Relaxed); + Ok(ModemStatus { + cts: false, + dsr: state & SERIAL_STATE_TX_CARRIER != 0, + ri: state & SERIAL_STATE_RING_SIGNAL != 0, + cd: state & SERIAL_STATE_RX_CARRIER != 0, + }) + } + + fn stop(&mut self) { + self.stop.store(true, Ordering::Relaxed); + if let Some(thread) = self.thread.take() { + let _ = thread.join(); + } + } +} + +impl Drop for CdcNotificationReader { + fn drop(&mut self) { + self.stop(); + } +} + +fn parse_serial_state_notification(data: &[u8]) -> std::result::Result, String> { + if data.len() < 8 { + return Err(format!( + "invalid CDC notification, expected at least 8 bytes, got {}", + data.len() + )); + } + if data[0] != NOTIFICATION_REQUEST_TYPE { + return Err(format!( + "invalid CDC notification request type 0x{:02x}", + data[0] + )); + } + if data[1] != SERIAL_STATE_NOTIFICATION { + return Ok(None); + } + + let payload_size = usize::from(u16::from_le_bytes([data[6], data[7]])); + if payload_size != 2 || data.len() != 8 + payload_size { + return Err(format!( + "invalid CDC serial-state notification, expected {SERIAL_STATE_NOTIFICATION_SIZE} bytes, got {}", + data.len() + )); + } + + Ok(Some(u16::from_le_bytes([data[8], data[9]]))) +} pub struct CdcAcmDriver { port_index: usize, @@ -20,7 +141,10 @@ pub struct CdcAcmDriver { rts: bool, endpoints: Option, transport: Option, + control_claimed: bool, + data_claimed: bool, reader: Option, + notification_reader: Option, } impl CdcAcmDriver { @@ -34,7 +158,10 @@ impl CdcAcmDriver { rts: false, endpoints: None, transport: None, + control_claimed: false, + data_claimed: false, reader: None, + notification_reader: None, } } @@ -118,23 +245,54 @@ fn resolve_iad_pair(transport: &SharedTransport, port_index: usize) -> Option<(u impl Driver for CdcAcmDriver { fn open(&mut self, transport: &SharedTransport) -> Result<()> { - self.transport = Some(transport.clone()); self.resolve_interfaces(transport)?; - transport.claim_interface(self.control_iface)?; - if self.data_iface != self.control_iface { - transport.claim_interface(self.data_iface)?; + self.transport = Some(transport.clone()); + + let result = (|| { + transport.claim_interface(self.control_iface)?; + self.control_claimed = true; + if self.data_iface != self.control_iface { + transport.claim_interface(self.data_iface)?; + self.data_claimed = true; + } + + if let Some(endpoint) = transport + .endpoints(self.control_iface) + .into_iter() + .find(|endpoint| endpoint.is_interrupt_in()) + { + let interrupt_in = + transport.open_interrupt_in(endpoint.address, endpoint.max_packet_size)?; + self.notification_reader = Some(CdcNotificationReader::start( + interrupt_in, + endpoint.max_packet_size, + )); + } + + self.endpoints = Some(EndpointPair::open(transport, self.data_iface)?); + Ok(()) + })(); + + if result.is_err() { + let _ = self.close(); } - self.endpoints = Some(EndpointPair::open(transport, self.data_iface)?); - Ok(()) + result } fn close(&mut self) -> Result<()> { if let Some(mut r) = self.reader.take() { r.stop(); } + if let Some(mut r) = self.notification_reader.take() { + r.stop(); + } if let Some(t) = &self.transport { - let _ = t.release_interface(self.data_iface); - if self.control_iface != self.data_iface { + if self.data_claimed { + self.data_claimed = false; + let _ = t.release_interface(self.data_iface); + } + if self.control_claimed { + self.control_claimed = false; let _ = t.release_interface(self.control_iface); } } @@ -187,7 +345,10 @@ impl Driver for CdcAcmDriver { } fn modem_status(&mut self) -> Result { - Ok(ModemStatus::default()) + self.notification_reader + .as_ref() + .map(CdcNotificationReader::modem_status) + .unwrap_or_else(|| Ok(ModemStatus::default())) } fn bulk_in_mps(&self) -> u16 { diff --git a/crates/android-usb-serial/tests/cdc_acm_test.rs b/crates/android-usb-serial/tests/cdc_acm_test.rs index 53b36d63..65821211 100644 --- a/crates/android-usb-serial/tests/cdc_acm_test.rs +++ b/crates/android-usb-serial/tests/cdc_acm_test.rs @@ -8,6 +8,7 @@ use android_usb_serial::drivers::line_coding_bytes; use android_usb_serial::fake::{FakeTransport, RecordedControl}; use android_usb_serial::transport::{EndpointInfo, InterfaceInfo, Transport}; use std::sync::Arc; +use std::time::{Duration, Instant}; fn open_on(fake: &FakeTransport, port_index: usize) -> android_usb_serial::port::SerialPortHandle { let transport: Arc = Arc::new(fake.clone()); @@ -110,8 +111,8 @@ fn set_line_coding_7e1_bulk_out() { } #[test] -fn unsupported_modem_status_returns_ok_false() { - let fake = FakeTransport::cdc_iad(); +fn modem_status_defaults_false_without_notification_endpoint() { + let fake = FakeTransport::cdc_single_iface(); let mut port = open_on(&fake, 0); let status = port.modem_status().expect("modem"); assert!(!status.cts); @@ -119,3 +120,106 @@ fn unsupported_modem_status_returns_ok_false() { assert!(!status.ri); assert!(!status.cd); } + +#[test] +fn notification_endpoint_open_failure_releases_interfaces_for_retry() { + let fake = FakeTransport::cdc_iad(); + let interrupt_in = fake + .open_interrupt_in(0x81, 64) + .expect("reserve interrupt endpoint"); + let transport: Arc = Arc::new(fake.clone()); + + assert!(open_port(transport.clone(), 0).is_err()); + assert!(fake.claimed_interfaces().is_empty()); + + drop(interrupt_in); + let port = open_port(transport, 0).expect("retry after initialization failure"); + drop(port); + assert!(fake.claimed_interfaces().is_empty()); +} + +#[test] +fn failure_after_notification_reader_starts_cleans_up() { + let fake = FakeTransport::cdc_iad(); + fake.configure_endpoints(&[ + ( + 0, + vec![EndpointInfo { + address: 0x81, + attributes: 3, + max_packet_size: 64, + interval: 1, + }], + ), + ( + 1, + vec![EndpointInfo { + address: 0x82, + attributes: 2, + max_packet_size: 64, + interval: 0, + }], + ), + ]); + let transport: Arc = Arc::new(fake.clone()); + + assert!(open_port(transport, 0).is_err()); + assert!(fake.claimed_interfaces().is_empty()); + fake.open_interrupt_in(0x81, 64) + .expect("notification endpoint released after initialization failure"); +} + +#[test] +fn serial_state_notifications_update_modem_status_and_release_endpoint() { + let fake = FakeTransport::cdc_iad(); + fake.push_interrupt_in(&[0xa1, 0x20, 0, 0, 0, 0, 2, 0, 0x0b, 0]); + + let mut port = open_on(&fake, 0); + let deadline = Instant::now() + Duration::from_secs(1); + let status = loop { + let status = port.modem_status().expect("modem"); + if status.cd && status.dsr && status.ri { + break status; + } + assert!(Instant::now() < deadline, "serial state was not updated"); + std::thread::sleep(Duration::from_millis(5)); + }; + assert!(!status.cts); + + fake.push_interrupt_in(&[0xa1, 0x20, 0, 0, 0, 0, 2, 0, 0, 0]); + let deadline = Instant::now() + Duration::from_secs(1); + loop { + let status = port.modem_status().expect("modem"); + if !status.cd && !status.dsr && !status.ri { + break; + } + assert!(Instant::now() < deadline, "serial state was not cleared"); + std::thread::sleep(Duration::from_millis(5)); + } + + drop(port); + fake.open_interrupt_in(0x81, 64) + .expect("interrupt endpoint released when the port closes"); +} + +#[test] +fn malformed_serial_state_notification_is_reported() { + let fake = FakeTransport::cdc_iad(); + fake.push_interrupt_in(&[0xa1, 0x20, 0, 0, 0, 0, 2, 0, 1]); + + let mut port = open_on(&fake, 0); + let deadline = Instant::now() + Duration::from_secs(1); + let error = loop { + match port.modem_status() { + Ok(_) => { + assert!( + Instant::now() < deadline, + "malformed notification was not reported" + ); + std::thread::sleep(Duration::from_millis(5)); + } + Err(error) => break error, + } + }; + assert!(error.to_string().contains("expected 10 bytes, got 9")); +}