redbear-acmd: fix global endpoint indexing; add serial-state monitor + SEND_BREAK (P6-A)
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).
This commit is contained in:
@@ -21,9 +21,49 @@ use xhcid_interface::{
|
|||||||
const SET_LINE_CODING: u8 = 0x20;
|
const SET_LINE_CODING: u8 = 0x20;
|
||||||
const GET_LINE_CODING: u8 = 0x21;
|
const GET_LINE_CODING: u8 = 0x21;
|
||||||
const SET_CONTROL_LINE_STATE: u8 = 0x22;
|
const SET_CONTROL_LINE_STATE: u8 = 0x22;
|
||||||
|
const SEND_BREAK: u8 = 0x23;
|
||||||
const CTRL_DTR: u16 = 1 << 0;
|
const CTRL_DTR: u16 = 1 << 0;
|
||||||
const CTRL_RTS: u16 = 1 << 1;
|
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)]
|
#[repr(C, packed)]
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
struct LineCoding { dw_dte_rate: u32, b_char_format: u8, b_parity_type: u8, b_data_bits: u8 }
|
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> {
|
fn set_control_line_state(&mut self, state: u16) -> Result<(), io::Error> {
|
||||||
self.cdc_ctrl_msg(SET_CONTROL_LINE_STATE, state, DeviceReqData::NoData)
|
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<std::sync::Mutex<SerialState>>) {
|
||||||
|
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() {
|
fn main() {
|
||||||
@@ -64,18 +141,37 @@ fn main() {
|
|||||||
let handle = XhciClientHandle::new(scheme.clone(), port_id).expect("xhci");
|
let handle = XhciClientHandle::new(scheme.clone(), port_id).expect("xhci");
|
||||||
let desc: DevDesc = handle.get_standard_descs().expect("descriptors");
|
let desc: DevDesc = handle.get_standard_descs().expect("descriptors");
|
||||||
let (conf_desc, data_if) = desc.config_descs.iter()
|
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");
|
.expect("No CDC ACM data interface");
|
||||||
handle.configure_endpoints(&ConfigureEndpointsReq {
|
handle.configure_endpoints(&ConfigureEndpointsReq {
|
||||||
config_desc: conf_desc.configuration_value, interface_desc: Some(data_if.number),
|
config_desc: conf_desc.configuration_value, interface_desc: Some(data_if.number),
|
||||||
alternate_setting: Some(data_if.alternate_setting), hub_ports: None,
|
alternate_setting: Some(data_if.alternate_setting), hub_ports: None,
|
||||||
}).expect("config");
|
}).expect("config");
|
||||||
let bulk_in_num = data_if.endpoints.iter()
|
|
||||||
.position(|ep| ep.direction() == EndpDirection::In && ep.ty() == EndpointTy::Bulk)
|
// xhcid keys endpoints by GLOBAL index across all interfaces of the
|
||||||
.map(|i| (i + 1) as u8).expect("bulk IN");
|
// configuration (see xhcid PortState::get_endp_desc / endpoint_states).
|
||||||
let bulk_out_num = data_if.endpoints.iter()
|
// Counting per-interface positions instead opens the wrong endpoints on
|
||||||
.position(|ep| ep.direction() == EndpDirection::Out && ep.ty() == EndpointTy::Bulk)
|
// two-interface ACM devices: with the comm interface's interrupt endpoint
|
||||||
.map(|i| (i + 1) as u8).expect("bulk OUT");
|
// 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 {
|
let mut dev = AcmDevice {
|
||||||
bulk_in: handle.open_endpoint(bulk_in_num).expect("open IN"),
|
bulk_in: handle.open_endpoint(bulk_in_num).expect("open IN"),
|
||||||
bulk_out: handle.open_endpoint(bulk_out_num).expect("open OUT"),
|
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_line_coding(&lc);
|
||||||
let _ = dev.set_control_line_state(CTRL_DTR | CTRL_RTS);
|
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) ──
|
// ── Scheme IPC path (Redox) ──
|
||||||
#[cfg(target_os = "redox")]
|
#[cfg(target_os = "redox")]
|
||||||
{
|
{
|
||||||
@@ -94,7 +205,11 @@ fn main() {
|
|||||||
log::info!("CDC ACM: registering /scheme/{}", scheme_name);
|
log::info!("CDC ACM: registering /scheme/{}", scheme_name);
|
||||||
let socket = Socket::create().expect("CDC ACM: scheme socket");
|
let socket = Socket::create().expect("CDC ACM: scheme socket");
|
||||||
let mut scheme_state = redox_scheme::scheme::SchemeState::new();
|
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);
|
let _ = libredox::call::setrens(0, 0);
|
||||||
log::info!("CDC ACM: scheme /scheme/{} ready", scheme_name);
|
log::info!("CDC ACM: scheme /scheme/{} ready", scheme_name);
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use redox_scheme::scheme::SchemeSync;
|
use redox_scheme::scheme::SchemeSync;
|
||||||
use redox_scheme::{CallerCtx, OpenResult};
|
use redox_scheme::{CallerCtx, OpenResult};
|
||||||
@@ -8,31 +8,54 @@ use syscall::schemev2::NewFdFlags;
|
|||||||
|
|
||||||
use xhcid_interface::XhciEndpHandle;
|
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 {
|
pub struct AcmScheme {
|
||||||
bulk_in: Mutex<XhciEndpHandle>,
|
bulk_in: Mutex<XhciEndpHandle>,
|
||||||
bulk_out: Mutex<XhciEndpHandle>,
|
bulk_out: Mutex<XhciEndpHandle>,
|
||||||
|
serial_state: Arc<Mutex<SerialState>>,
|
||||||
open_count: Mutex<usize>,
|
open_count: Mutex<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AcmScheme {
|
impl AcmScheme {
|
||||||
pub fn new(bulk_in: XhciEndpHandle, bulk_out: XhciEndpHandle) -> Self {
|
pub fn new(bulk_in: XhciEndpHandle, bulk_out: XhciEndpHandle, serial_state: Arc<Mutex<SerialState>>) -> Self {
|
||||||
Self { bulk_in: Mutex::new(bulk_in), bulk_out: Mutex::new(bulk_out), open_count: Mutex::new(0) }
|
Self {
|
||||||
|
bulk_in: Mutex::new(bulk_in),
|
||||||
|
bulk_out: Mutex::new(bulk_out),
|
||||||
|
serial_state,
|
||||||
|
open_count: Mutex::new(0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SchemeSync for AcmScheme {
|
impl SchemeSync for AcmScheme {
|
||||||
fn openat(&mut self, fd: usize, path: &str, _flags: usize, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result<OpenResult> {
|
fn openat(&mut self, fd: usize, path: &str, _flags: usize, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result<OpenResult> {
|
||||||
if fd != 1 { return Err(Error::new(EBADF)); }
|
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 == "/" {
|
if path.is_empty() || path == "." || path == "/" {
|
||||||
*self.open_count.lock().unwrap_or_else(|e| e.into_inner()) += 1;
|
*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) });
|
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 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 })
|
Ok(OpenResult::OtherScheme { fd: next_id })
|
||||||
}
|
}
|
||||||
fn read(&mut self, id: usize, buf: &mut [u8], _offset: u64, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result<usize> {
|
fn read(&mut self, id: usize, buf: &mut [u8], _offset: u64, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result<usize> {
|
||||||
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());
|
let mut bulk_in = self.bulk_in.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
match bulk_in.transfer_read(buf) {
|
match bulk_in.transfer_read(buf) {
|
||||||
Ok(status) if status.bytes_transferred > 0 => Ok(status.bytes_transferred as usize),
|
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<usize> {
|
fn write(&mut self, id: usize, buf: &[u8], _offset: u64, _fcntl_flags: u32, _ctx: &CallerCtx) -> Result<usize> {
|
||||||
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());
|
let mut bulk_out = self.bulk_out.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
match bulk_out.transfer_write(buf) {
|
match bulk_out.transfer_write(buf) {
|
||||||
Ok(status) if status.bytes_transferred > 0 => Ok(status.bytes_transferred as usize),
|
Ok(status) if status.bytes_transferred > 0 => Ok(status.bytes_transferred as usize),
|
||||||
|
|||||||
Reference in New Issue
Block a user