Skip to content

Commit 0f5a944

Browse files
authored
fix(cdc-acm): expose serial state notifications (#37)
1 parent f15973f commit 0f5a944

3 files changed

Lines changed: 283 additions & 13 deletions

File tree

crates/android-usb-serial/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Track CDC ACM interrupt-IN notifications and expose DCD, DSR, and ring state
6+
through the modem-status API
7+
38
## 0.1.0
49

510
- Initial release: FTDI, CP21xx, CH34x, Prolific, CDC-ACM, GSM modem, Chrome CCD drivers on `nusb`

crates/android-usb-serial/src/drivers/cdc_acm.rs

Lines changed: 172 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,135 @@
22
33
use super::{line_coding_bytes, Driver, EndpointPair, ModemStatus, WRITE_TIMEOUT_MS};
44
use crate::config::{FlowControl, LineConfig, PurgeKind};
5-
use crate::error::{Result, UsbSerialError};
5+
use crate::error::{ReadOutcome, Result, UsbSerialError};
66
use crate::reader::SerialReader;
7-
use crate::transport::{ControlRequest, SharedTransport, USB_RECIP_INTERFACE, USB_TYPE_CLASS};
7+
use crate::transport::{
8+
BulkIn, ControlRequest, SharedTransport, USB_RECIP_INTERFACE, USB_TYPE_CLASS,
9+
};
10+
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
11+
use std::sync::{Arc, Mutex};
12+
use std::thread::{self, JoinHandle};
813

914
const USB_SUBCLASS_ACM: u8 = 2;
1015
const SET_LINE_CODING: u8 = 0x20;
1116
const SET_CONTROL_LINE_STATE: u8 = 0x22;
1217
const SEND_BREAK: u8 = 0x23;
18+
const NOTIFICATION_REQUEST_TYPE: u8 = 0xa1;
19+
const SERIAL_STATE_NOTIFICATION: u8 = 0x20;
20+
const SERIAL_STATE_NOTIFICATION_SIZE: usize = 10;
21+
const SERIAL_STATE_RX_CARRIER: u16 = 1 << 0;
22+
const SERIAL_STATE_TX_CARRIER: u16 = 1 << 1;
23+
const SERIAL_STATE_RING_SIGNAL: u16 = 1 << 3;
24+
const NOTIFICATION_READ_TIMEOUT_MS: u32 = 200;
25+
26+
struct CdcNotificationReader {
27+
state: Arc<AtomicU16>,
28+
error: Arc<Mutex<Option<String>>>,
29+
stop: Arc<AtomicBool>,
30+
thread: Option<JoinHandle<()>>,
31+
}
32+
33+
impl CdcNotificationReader {
34+
fn start(mut endpoint: Box<dyn BulkIn>, max_packet_size: u16) -> Self {
35+
let state = Arc::new(AtomicU16::new(0));
36+
let error = Arc::new(Mutex::new(None));
37+
let stop = Arc::new(AtomicBool::new(false));
38+
let thread_state = Arc::clone(&state);
39+
let thread_error = Arc::clone(&error);
40+
let thread_stop = Arc::clone(&stop);
41+
let buffer_size = usize::from(max_packet_size).max(SERIAL_STATE_NOTIFICATION_SIZE);
42+
43+
let thread = thread::spawn(move || {
44+
let mut buffer = vec![0; buffer_size];
45+
while !thread_stop.load(Ordering::Relaxed) {
46+
match endpoint.read(&mut buffer, NOTIFICATION_READ_TIMEOUT_MS) {
47+
Ok(ReadOutcome::Data(data)) if !data.is_empty() => {
48+
match parse_serial_state_notification(&data) {
49+
Ok(Some(serial_state)) => {
50+
thread_state.store(serial_state, Ordering::Relaxed);
51+
}
52+
Ok(None) => {}
53+
Err(error) => {
54+
*thread_error.lock().unwrap() = Some(error);
55+
break;
56+
}
57+
}
58+
}
59+
Ok(ReadOutcome::TimedOut) | Ok(ReadOutcome::Data(_)) => {}
60+
Ok(ReadOutcome::Cancelled) => break,
61+
Err(error) => {
62+
if !thread_stop.load(Ordering::Relaxed) {
63+
*thread_error.lock().unwrap() = Some(error.to_string());
64+
}
65+
break;
66+
}
67+
}
68+
}
69+
});
70+
71+
Self {
72+
state,
73+
error,
74+
stop,
75+
thread: Some(thread),
76+
}
77+
}
78+
79+
fn modem_status(&self) -> Result<ModemStatus> {
80+
if let Some(error) = self.error.lock().unwrap().take() {
81+
return Err(UsbSerialError::Io(error));
82+
}
83+
84+
let state = self.state.load(Ordering::Relaxed);
85+
Ok(ModemStatus {
86+
cts: false,
87+
dsr: state & SERIAL_STATE_TX_CARRIER != 0,
88+
ri: state & SERIAL_STATE_RING_SIGNAL != 0,
89+
cd: state & SERIAL_STATE_RX_CARRIER != 0,
90+
})
91+
}
92+
93+
fn stop(&mut self) {
94+
self.stop.store(true, Ordering::Relaxed);
95+
if let Some(thread) = self.thread.take() {
96+
let _ = thread.join();
97+
}
98+
}
99+
}
100+
101+
impl Drop for CdcNotificationReader {
102+
fn drop(&mut self) {
103+
self.stop();
104+
}
105+
}
106+
107+
fn parse_serial_state_notification(data: &[u8]) -> std::result::Result<Option<u16>, String> {
108+
if data.len() < 8 {
109+
return Err(format!(
110+
"invalid CDC notification, expected at least 8 bytes, got {}",
111+
data.len()
112+
));
113+
}
114+
if data[0] != NOTIFICATION_REQUEST_TYPE {
115+
return Err(format!(
116+
"invalid CDC notification request type 0x{:02x}",
117+
data[0]
118+
));
119+
}
120+
if data[1] != SERIAL_STATE_NOTIFICATION {
121+
return Ok(None);
122+
}
123+
124+
let payload_size = usize::from(u16::from_le_bytes([data[6], data[7]]));
125+
if payload_size != 2 || data.len() != 8 + payload_size {
126+
return Err(format!(
127+
"invalid CDC serial-state notification, expected {SERIAL_STATE_NOTIFICATION_SIZE} bytes, got {}",
128+
data.len()
129+
));
130+
}
131+
132+
Ok(Some(u16::from_le_bytes([data[8], data[9]])))
133+
}
13134

14135
pub struct CdcAcmDriver {
15136
port_index: usize,
@@ -20,7 +141,10 @@ pub struct CdcAcmDriver {
20141
rts: bool,
21142
endpoints: Option<EndpointPair>,
22143
transport: Option<SharedTransport>,
144+
control_claimed: bool,
145+
data_claimed: bool,
23146
reader: Option<SerialReader>,
147+
notification_reader: Option<CdcNotificationReader>,
24148
}
25149

26150
impl CdcAcmDriver {
@@ -34,7 +158,10 @@ impl CdcAcmDriver {
34158
rts: false,
35159
endpoints: None,
36160
transport: None,
161+
control_claimed: false,
162+
data_claimed: false,
37163
reader: None,
164+
notification_reader: None,
38165
}
39166
}
40167

@@ -118,23 +245,54 @@ fn resolve_iad_pair(transport: &SharedTransport, port_index: usize) -> Option<(u
118245

119246
impl Driver for CdcAcmDriver {
120247
fn open(&mut self, transport: &SharedTransport) -> Result<()> {
121-
self.transport = Some(transport.clone());
122248
self.resolve_interfaces(transport)?;
123-
transport.claim_interface(self.control_iface)?;
124-
if self.data_iface != self.control_iface {
125-
transport.claim_interface(self.data_iface)?;
249+
self.transport = Some(transport.clone());
250+
251+
let result = (|| {
252+
transport.claim_interface(self.control_iface)?;
253+
self.control_claimed = true;
254+
if self.data_iface != self.control_iface {
255+
transport.claim_interface(self.data_iface)?;
256+
self.data_claimed = true;
257+
}
258+
259+
if let Some(endpoint) = transport
260+
.endpoints(self.control_iface)
261+
.into_iter()
262+
.find(|endpoint| endpoint.is_interrupt_in())
263+
{
264+
let interrupt_in =
265+
transport.open_interrupt_in(endpoint.address, endpoint.max_packet_size)?;
266+
self.notification_reader = Some(CdcNotificationReader::start(
267+
interrupt_in,
268+
endpoint.max_packet_size,
269+
));
270+
}
271+
272+
self.endpoints = Some(EndpointPair::open(transport, self.data_iface)?);
273+
Ok(())
274+
})();
275+
276+
if result.is_err() {
277+
let _ = self.close();
126278
}
127-
self.endpoints = Some(EndpointPair::open(transport, self.data_iface)?);
128-
Ok(())
279+
result
129280
}
130281

131282
fn close(&mut self) -> Result<()> {
132283
if let Some(mut r) = self.reader.take() {
133284
r.stop();
134285
}
286+
if let Some(mut r) = self.notification_reader.take() {
287+
r.stop();
288+
}
135289
if let Some(t) = &self.transport {
136-
let _ = t.release_interface(self.data_iface);
137-
if self.control_iface != self.data_iface {
290+
if self.data_claimed {
291+
self.data_claimed = false;
292+
let _ = t.release_interface(self.data_iface);
293+
}
294+
if self.control_claimed {
295+
self.control_claimed = false;
138296
let _ = t.release_interface(self.control_iface);
139297
}
140298
}
@@ -187,7 +345,10 @@ impl Driver for CdcAcmDriver {
187345
}
188346

189347
fn modem_status(&mut self) -> Result<ModemStatus> {
190-
Ok(ModemStatus::default())
348+
self.notification_reader
349+
.as_ref()
350+
.map(CdcNotificationReader::modem_status)
351+
.unwrap_or_else(|| Ok(ModemStatus::default()))
191352
}
192353

193354
fn bulk_in_mps(&self) -> u16 {

crates/android-usb-serial/tests/cdc_acm_test.rs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use android_usb_serial::drivers::line_coding_bytes;
88
use android_usb_serial::fake::{FakeTransport, RecordedControl};
99
use android_usb_serial::transport::{EndpointInfo, InterfaceInfo, Transport};
1010
use std::sync::Arc;
11+
use std::time::{Duration, Instant};
1112

1213
fn open_on(fake: &FakeTransport, port_index: usize) -> android_usb_serial::port::SerialPortHandle {
1314
let transport: Arc<dyn Transport> = Arc::new(fake.clone());
@@ -110,12 +111,115 @@ fn set_line_coding_7e1_bulk_out() {
110111
}
111112

112113
#[test]
113-
fn unsupported_modem_status_returns_ok_false() {
114-
let fake = FakeTransport::cdc_iad();
114+
fn modem_status_defaults_false_without_notification_endpoint() {
115+
let fake = FakeTransport::cdc_single_iface();
115116
let mut port = open_on(&fake, 0);
116117
let status = port.modem_status().expect("modem");
117118
assert!(!status.cts);
118119
assert!(!status.dsr);
119120
assert!(!status.ri);
120121
assert!(!status.cd);
121122
}
123+
124+
#[test]
125+
fn notification_endpoint_open_failure_releases_interfaces_for_retry() {
126+
let fake = FakeTransport::cdc_iad();
127+
let interrupt_in = fake
128+
.open_interrupt_in(0x81, 64)
129+
.expect("reserve interrupt endpoint");
130+
let transport: Arc<dyn Transport> = Arc::new(fake.clone());
131+
132+
assert!(open_port(transport.clone(), 0).is_err());
133+
assert!(fake.claimed_interfaces().is_empty());
134+
135+
drop(interrupt_in);
136+
let port = open_port(transport, 0).expect("retry after initialization failure");
137+
drop(port);
138+
assert!(fake.claimed_interfaces().is_empty());
139+
}
140+
141+
#[test]
142+
fn failure_after_notification_reader_starts_cleans_up() {
143+
let fake = FakeTransport::cdc_iad();
144+
fake.configure_endpoints(&[
145+
(
146+
0,
147+
vec![EndpointInfo {
148+
address: 0x81,
149+
attributes: 3,
150+
max_packet_size: 64,
151+
interval: 1,
152+
}],
153+
),
154+
(
155+
1,
156+
vec![EndpointInfo {
157+
address: 0x82,
158+
attributes: 2,
159+
max_packet_size: 64,
160+
interval: 0,
161+
}],
162+
),
163+
]);
164+
let transport: Arc<dyn Transport> = Arc::new(fake.clone());
165+
166+
assert!(open_port(transport, 0).is_err());
167+
assert!(fake.claimed_interfaces().is_empty());
168+
fake.open_interrupt_in(0x81, 64)
169+
.expect("notification endpoint released after initialization failure");
170+
}
171+
172+
#[test]
173+
fn serial_state_notifications_update_modem_status_and_release_endpoint() {
174+
let fake = FakeTransport::cdc_iad();
175+
fake.push_interrupt_in(&[0xa1, 0x20, 0, 0, 0, 0, 2, 0, 0x0b, 0]);
176+
177+
let mut port = open_on(&fake, 0);
178+
let deadline = Instant::now() + Duration::from_secs(1);
179+
let status = loop {
180+
let status = port.modem_status().expect("modem");
181+
if status.cd && status.dsr && status.ri {
182+
break status;
183+
}
184+
assert!(Instant::now() < deadline, "serial state was not updated");
185+
std::thread::sleep(Duration::from_millis(5));
186+
};
187+
assert!(!status.cts);
188+
189+
fake.push_interrupt_in(&[0xa1, 0x20, 0, 0, 0, 0, 2, 0, 0, 0]);
190+
let deadline = Instant::now() + Duration::from_secs(1);
191+
loop {
192+
let status = port.modem_status().expect("modem");
193+
if !status.cd && !status.dsr && !status.ri {
194+
break;
195+
}
196+
assert!(Instant::now() < deadline, "serial state was not cleared");
197+
std::thread::sleep(Duration::from_millis(5));
198+
}
199+
200+
drop(port);
201+
fake.open_interrupt_in(0x81, 64)
202+
.expect("interrupt endpoint released when the port closes");
203+
}
204+
205+
#[test]
206+
fn malformed_serial_state_notification_is_reported() {
207+
let fake = FakeTransport::cdc_iad();
208+
fake.push_interrupt_in(&[0xa1, 0x20, 0, 0, 0, 0, 2, 0, 1]);
209+
210+
let mut port = open_on(&fake, 0);
211+
let deadline = Instant::now() + Duration::from_secs(1);
212+
let error = loop {
213+
match port.modem_status() {
214+
Ok(_) => {
215+
assert!(
216+
Instant::now() < deadline,
217+
"malformed notification was not reported"
218+
);
219+
std::thread::sleep(Duration::from_millis(5));
220+
}
221+
Err(error) => break error,
222+
}
223+
};
224+
assert!(error.to_string().contains("expected 10 bytes, got 9"));
225+
}

0 commit comments

Comments
 (0)