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
5 changes: 5 additions & 0 deletions crates/android-usb-serial/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
183 changes: 172 additions & 11 deletions crates/android-usb-serial/src/drivers/cdc_acm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AtomicU16>,
error: Arc<Mutex<Option<String>>>,
stop: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}

impl CdcNotificationReader {
fn start(mut endpoint: Box<dyn BulkIn>, 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<ModemStatus> {
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<Option<u16>, 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,
Expand All @@ -20,7 +141,10 @@ pub struct CdcAcmDriver {
rts: bool,
endpoints: Option<EndpointPair>,
transport: Option<SharedTransport>,
control_claimed: bool,
data_claimed: bool,
reader: Option<SerialReader>,
notification_reader: Option<CdcNotificationReader>,
}

impl CdcAcmDriver {
Expand All @@ -34,7 +158,10 @@ impl CdcAcmDriver {
rts: false,
endpoints: None,
transport: None,
control_claimed: false,
data_claimed: false,
reader: None,
notification_reader: None,
}
}

Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -187,7 +345,10 @@ impl Driver for CdcAcmDriver {
}

fn modem_status(&mut self) -> Result<ModemStatus> {
Ok(ModemStatus::default())
self.notification_reader
.as_ref()
.map(CdcNotificationReader::modem_status)
.unwrap_or_else(|| Ok(ModemStatus::default()))
}

fn bulk_in_mps(&self) -> u16 {
Expand Down
108 changes: 106 additions & 2 deletions crates/android-usb-serial/tests/cdc_acm_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Transport> = Arc::new(fake.clone());
Expand Down Expand Up @@ -110,12 +111,115 @@ 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);
assert!(!status.dsr);
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<dyn Transport> = 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<dyn Transport> = 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"));
}