From f505d18a8989fa78e583abc1e0152bb7c7836b76 Mon Sep 17 00:00:00 2001 From: vasilito Date: Mon, 20 Jul 2026 20:59:16 +0900 Subject: [PATCH] redbear-acmd: fix global endpoint indexing; add serial-state monitor + SEND_BREAK (P6-A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fix: endpoint numbers were computed as per-interface positions, but xhcid keys endpoints by GLOBAL index across all interfaces of the configuration. On two-interface ACM devices (comm interrupt-IN first), the driver opened (interrupt-IN, bulk-IN) as (bulk_in, bulk_out) — read from the interrupt endpoint and wrote to the IN endpoint. Endpoints are now counted across all interfaces in configuration order, exactly as xhcid's PortState::get_endp_desc does. P6-A expansion (Linux 7.1 cdc-acm.c reference): - SEND_BREAK (0x23) control request - SERIAL_STATE monitoring: poll the comm interface's interrupt-IN endpoint on a dedicated thread, parse the 8-byte header + 2-byte UART state bitmap (CDC 1.1 6.3.5): DCD/DSR/break/RI/framing/parity/overrun, log transitions - scheme gains a read-only 'state' file reporting the current line state (dcd=.. dsr=.. ...) for getty/terminal consumers Verified: cargo check -Z build-std --target x86_64-unknown-redox clean, no new warnings. Runtime validation needs an ACM device (QEMU has no CDC ACM emulation; FTDI/Arduino on bare metal or passed through). --- .../system/redbear-acmd/source/src/main.rs | 131 ++++++++++++++++-- .../system/redbear-acmd/source/src/scheme.rs | 35 ++++- 2 files changed, 152 insertions(+), 14 deletions(-) diff --git a/local/recipes/system/redbear-acmd/source/src/main.rs b/local/recipes/system/redbear-acmd/source/src/main.rs index 8bbfa6e0a0..6efd3b3b53 100644 --- a/local/recipes/system/redbear-acmd/source/src/main.rs +++ b/local/recipes/system/redbear-acmd/source/src/main.rs @@ -21,9 +21,49 @@ use xhcid_interface::{ const SET_LINE_CODING: u8 = 0x20; const GET_LINE_CODING: u8 = 0x21; const SET_CONTROL_LINE_STATE: u8 = 0x22; +const SEND_BREAK: u8 = 0x23; const CTRL_DTR: u16 = 1 << 0; const CTRL_RTS: u16 = 1 << 1; +/// CDC ACM class codes. +const CDC_COMM_CLASS: u8 = 0x02; +const CDC_DATA_CLASS: u8 = 0x0A; + +/// UART state bitmap from the CDC SERIAL_STATE notification +/// (CDC spec 1.1 section 6.3.5, table 69). +#[derive(Clone, Copy, Debug, Default)] +pub struct SerialState { + pub dcd: bool, + pub dsr: bool, + pub break_detected: bool, + pub ring: bool, + pub framing_error: bool, + pub parity_error: bool, + pub overrun: bool, +} + +impl SerialState { + fn from_uart_state_bits(bits: u16) -> Self { + Self { + dcd: bits & (1 << 0) != 0, + dsr: bits & (1 << 1) != 0, + break_detected: bits & (1 << 2) != 0, + ring: bits & (1 << 3) != 0, + framing_error: bits & (1 << 4) != 0, + parity_error: bits & (1 << 5) != 0, + overrun: bits & (1 << 6) != 0, + } + } + pub fn to_display(&self) -> String { + format!( + "dcd={} dsr={} break={} ri={} framing={} parity={} overrun={}\n", + self.dcd as u8, self.dsr as u8, self.break_detected as u8, + self.ring as u8, self.framing_error as u8, self.parity_error as u8, + self.overrun as u8 + ) + } +} + #[repr(C, packed)] #[derive(Clone, Copy, Debug, Default)] struct LineCoding { dw_dte_rate: u32, b_char_format: u8, b_parity_type: u8, b_data_bits: u8 } @@ -50,6 +90,43 @@ impl AcmDevice { fn set_control_line_state(&mut self, state: u16) -> Result<(), io::Error> { self.cdc_ctrl_msg(SET_CONTROL_LINE_STATE, state, DeviceReqData::NoData) } + /// SEND_BREAK (CDC ACM 6.3.4): assert a break condition for `duration_ms` + /// (0xFFFF = indefinite, 0 = stop). + fn send_break(&mut self, duration_ms: u16) -> Result<(), io::Error> { + self.cdc_ctrl_msg(SEND_BREAK, duration_ms, DeviceReqData::NoData) + } +} + +/// SERIAL_STATE notification header size (8-byte setup-style header) plus the +/// 2-byte UART state bitmap (CDC spec 1.1 section 6.3.5). +const SERIAL_STATE_LEN: usize = 10; + +/// Poll the comm interface's interrupt-IN endpoint for SERIAL_STATE +/// notifications and keep `state` current. Runs for the life of the driver. +fn serial_state_monitor(mut int_in: XhciEndpHandle, state: std::sync::Arc>) { + let mut buf = [0u8; SERIAL_STATE_LEN]; + loop { + match int_in.transfer_read(&mut buf) { + Ok(s) if s.bytes_transferred as usize >= SERIAL_STATE_LEN => { + // bmRequestType 0xA1, bNotification 0x20 (SERIAL_STATE), + // wLength 2; UART state bitmap at bytes 8-9 (LE). + if buf[0] == 0xA1 && buf[1] == 0x20 { + let bits = u16::from_le_bytes([buf[8], buf[9]]); + let new_state = SerialState::from_uart_state_bits(bits); + let mut guard = state.lock().unwrap_or_else(|e| e.into_inner()); + if new_state.to_display() != guard.to_display() { + log::info!("CDC ACM: serial state: {}", new_state.to_display().trim_end()); + *guard = new_state; + } + } + } + Ok(_) => {} + Err(e) => { + log::warn!("CDC ACM: serial-state interrupt read: {}", e); + thread::sleep(time::Duration::from_millis(100)); + } + } + } } fn main() { @@ -64,18 +141,37 @@ fn main() { let handle = XhciClientHandle::new(scheme.clone(), port_id).expect("xhci"); let desc: DevDesc = handle.get_standard_descs().expect("descriptors"); let (conf_desc, data_if) = desc.config_descs.iter() - .find_map(|c| c.interface_descs.iter().find(|i| i.class == 0x0A).map(|d| (c.clone(), d.clone()))) + .find_map(|c| c.interface_descs.iter().find(|i| i.class == CDC_DATA_CLASS).map(|d| (c.clone(), d.clone()))) .expect("No CDC ACM data interface"); handle.configure_endpoints(&ConfigureEndpointsReq { config_desc: conf_desc.configuration_value, interface_desc: Some(data_if.number), alternate_setting: Some(data_if.alternate_setting), hub_ports: None, }).expect("config"); - let bulk_in_num = data_if.endpoints.iter() - .position(|ep| ep.direction() == EndpDirection::In && ep.ty() == EndpointTy::Bulk) - .map(|i| (i + 1) as u8).expect("bulk IN"); - let bulk_out_num = data_if.endpoints.iter() - .position(|ep| ep.direction() == EndpDirection::Out && ep.ty() == EndpointTy::Bulk) - .map(|i| (i + 1) as u8).expect("bulk OUT"); + + // xhcid keys endpoints by GLOBAL index across all interfaces of the + // configuration (see xhcid PortState::get_endp_desc / endpoint_states). + // Counting per-interface positions instead opens the wrong endpoints on + // two-interface ACM devices: with the comm interface's interrupt endpoint + // first, per-interface positions (1, 2) hit (interrupt-IN, bulk-IN) while + // the real bulk endpoints are (2, 3). Count across all interfaces in + // configuration order, exactly as xhcid does. + let mut bulk_in_num = None; + let mut bulk_out_num = None; + let mut int_in_num = None; + let mut global_num = 0u8; + for if_desc in conf_desc.interface_descs.iter() { + for ep in if_desc.endpoints.iter() { + global_num += 1; + match (ep.direction(), ep.ty(), if_desc.class) { + (EndpDirection::In, EndpointTy::Bulk, CDC_DATA_CLASS) => bulk_in_num = Some(global_num), + (EndpDirection::Out, EndpointTy::Bulk, CDC_DATA_CLASS) => bulk_out_num = Some(global_num), + (EndpDirection::In, EndpointTy::Interrupt, CDC_COMM_CLASS) => int_in_num = Some(global_num), + _ => {} + } + } + } + let bulk_in_num = bulk_in_num.expect("bulk IN"); + let bulk_out_num = bulk_out_num.expect("bulk OUT"); let mut dev = AcmDevice { bulk_in: handle.open_endpoint(bulk_in_num).expect("open IN"), bulk_out: handle.open_endpoint(bulk_out_num).expect("open OUT"), @@ -85,6 +181,21 @@ fn main() { let _ = dev.set_line_coding(&lc); let _ = dev.set_control_line_state(CTRL_DTR | CTRL_RTS); + // SERIAL_STATE monitoring on the comm interface's interrupt-IN endpoint + // (P6-A): DCD/DSR/break/RI/error bits, exposed via the scheme's "state" + // file. Devices without a comm interrupt endpoint simply report the + // default zero state. + let serial_state = std::sync::Arc::new(std::sync::Mutex::new(SerialState::default())); + if let Some(int_num) = int_in_num { + match dev.handle.open_endpoint(int_num) { + Ok(int_in) => { + let state_clone = std::sync::Arc::clone(&serial_state); + thread::spawn(move || serial_state_monitor(int_in, state_clone)); + } + Err(err) => log::warn!("CDC ACM: failed to open interrupt endpoint {}: {}", int_num, err), + } + } + // ── Scheme IPC path (Redox) ── #[cfg(target_os = "redox")] { @@ -94,7 +205,11 @@ fn main() { log::info!("CDC ACM: registering /scheme/{}", scheme_name); let socket = Socket::create().expect("CDC ACM: scheme socket"); let mut scheme_state = redox_scheme::scheme::SchemeState::new(); - let mut acm_scheme = crate::scheme::AcmScheme::new(dev.bulk_in, dev.bulk_out); + let mut acm_scheme = crate::scheme::AcmScheme::new( + dev.bulk_in, + dev.bulk_out, + std::sync::Arc::clone(&serial_state), + ); let _ = libredox::call::setrens(0, 0); log::info!("CDC ACM: scheme /scheme/{} ready", scheme_name); loop { diff --git a/local/recipes/system/redbear-acmd/source/src/scheme.rs b/local/recipes/system/redbear-acmd/source/src/scheme.rs index 7568065f48..a29811c5cc 100644 --- a/local/recipes/system/redbear-acmd/source/src/scheme.rs +++ b/local/recipes/system/redbear-acmd/source/src/scheme.rs @@ -1,4 +1,4 @@ -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use redox_scheme::scheme::SchemeSync; use redox_scheme::{CallerCtx, OpenResult}; @@ -8,31 +8,54 @@ use syscall::schemev2::NewFdFlags; use xhcid_interface::XhciEndpHandle; +use crate::SerialState; + +/// Scheme fd number for the read-only "state" file (serial line state). +const STATE_FD: usize = 2; + pub struct AcmScheme { bulk_in: Mutex, bulk_out: Mutex, + serial_state: Arc>, open_count: Mutex, } impl AcmScheme { - pub fn new(bulk_in: XhciEndpHandle, bulk_out: XhciEndpHandle) -> Self { - Self { bulk_in: Mutex::new(bulk_in), bulk_out: Mutex::new(bulk_out), open_count: Mutex::new(0) } + pub fn new(bulk_in: XhciEndpHandle, bulk_out: XhciEndpHandle, serial_state: Arc>) -> Self { + Self { + bulk_in: Mutex::new(bulk_in), + bulk_out: Mutex::new(bulk_out), + serial_state, + open_count: Mutex::new(0), + } } } impl SchemeSync for AcmScheme { fn openat(&mut self, fd: usize, path: &str, _flags: usize, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result { if fd != 1 { return Err(Error::new(EBADF)); } + if path == "state" { + return Ok(OpenResult::ThisScheme { number: STATE_FD, flags: NewFdFlags::from_bits_truncate(O_STAT as u8) }); + } if path.is_empty() || path == "." || path == "/" { *self.open_count.lock().unwrap_or_else(|e| e.into_inner()) += 1; return Ok(OpenResult::ThisScheme { number: 1, flags: NewFdFlags::from_bits_truncate(O_STAT as u8) }); } let mut count = self.open_count.lock().unwrap_or_else(|e| e.into_inner()); - let next_id = 2 + *count; *count += 1; + let next_id = 3 + *count; *count += 1; Ok(OpenResult::OtherScheme { fd: next_id }) } fn read(&mut self, id: usize, buf: &mut [u8], _offset: u64, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result { - if id < 2 || buf.is_empty() { return Err(Error::new(EBADF)); } + if buf.is_empty() { return Err(Error::new(EBADF)); } + if id == STATE_FD { + let state = self.serial_state.lock().unwrap_or_else(|e| e.into_inner()); + let text = state.to_display(); + let bytes = text.as_bytes(); + let n = bytes.len().min(buf.len()); + buf[..n].copy_from_slice(&bytes[..n]); + return Ok(n); + } + if id < 3 { return Err(Error::new(EBADF)); } let mut bulk_in = self.bulk_in.lock().unwrap_or_else(|e| e.into_inner()); match bulk_in.transfer_read(buf) { Ok(status) if status.bytes_transferred > 0 => Ok(status.bytes_transferred as usize), @@ -41,7 +64,7 @@ impl SchemeSync for AcmScheme { } } fn write(&mut self, id: usize, buf: &[u8], _offset: u64, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result { - if id < 2 || buf.is_empty() { return Err(Error::new(EBADF)); } + if id < 3 || buf.is_empty() { return Err(Error::new(EBADF)); } let mut bulk_out = self.bulk_out.lock().unwrap_or_else(|e| e.into_inner()); match bulk_out.transfer_write(buf) { Ok(status) if status.bytes_transferred > 0 => Ok(status.bytes_transferred as usize),