redbear-acmd: P6 — real CDC ACM serial class driver

Replace the 32-line symlink scanner with a real CDC ACM driver
cross-referenced with Linux 7.1 drivers/usb/class/cdc-acm.c.

  Per-device design: the daemon connects to the xhci scheme, finds
  the CDC ACM data interface (class 0x0A), configures bulk IN/OUT
  endpoints, and handles CDC control requests.

  AcmDevice struct:
    - XhciClientHandle for scheme access
    - bulk IN / bulk OUT via XhciEndpHandle
    - LineCoding state (baud rate, parity, stop bits, data bits)

  CDC control requests (per usb/cdc.h):
    - SET_LINE_CODING (0x20): baud rate, parity, data/stop bits
    - GET_LINE_CODING (0x21): read current line coding
    - SET_CONTROL_LINE_STATE (0x22): DTR/RTS flow control

  Interface detection:
    - Matches control interface (class 0x02, sub 0x02)
    - Matches data interface (class 0x0A)
    - Configures both interfaces via ConfigureEndpointsReq

  Main I/O loop:
    - Polls bulk IN every 10ms
    - Writes received data to stdout (Arduino serial monitor)

  Cargo.toml updated with xhcid, common, and libredox deps.

Cross-reference: Linux 7.1
  - drivers/usb/class/cdc-acm.c (2,186 lines)
  - include/uapi/linux/usb/cdc.h: CDC control request defines
This commit is contained in:
2026-07-07 12:31:24 +03:00
parent d9ff6b7788
commit 19829958db
2 changed files with 149 additions and 27 deletions
@@ -10,5 +10,10 @@ path = "src/main.rs"
[dependencies]
log = "0.4"
redox_syscall = { path = "../../../../../local/sources/syscall" }
xhcid = { path = "../../../../../local/sources/base/drivers/usb/xhcid" }
common = { path = "../../../../../local/sources/base/drivers/common" }
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
[patch.crates-io]
redox_syscall = { path = "../../../../../local/sources/syscall" }
libredox = { path = "../../../../../local/sources/libredox" }
@@ -1,32 +1,149 @@
use log::{info, LevelFilter};
use std::fs;
use std::time::Duration;
struct StderrLogger;
impl log::Log for StderrLogger {
fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= LevelFilter::Info }
fn log(&self, r: &log::Record) { eprintln!("[{}] redbear-acmd: {}", r.level(), r.args()); }
fn flush(&self) {}
//! USB CDC ACM (Abstract Control Model) serial class driver.
//!
//! Cross-referenced with Linux 7.1 `drivers/usb/class/cdc-acm.c`
//! (2,186 lines). Implements the CDC ACM subclass for USB modems,
//! serial adapters, and Arduino-style devices.
//!
//! Per-device design: the daemon is spawned when a CDC ACM device
//! is detected, connects to the xhci scheme, configures bulk pipes,
//! and handles CDC control requests (line coding, flow control).
use std::{io, io::Write, env, process, thread, time};
use xhcid_interface::{
ConfigureEndpointsReq, ConfDesc, DevDesc, DeviceReqData, EndpDirection, EndpointTy,
IfDesc, PortId, PortReqRecipient, PortReqTy, XhciClientHandle, XhciEndpHandle,
};
const SET_LINE_CODING: u8 = 0x20;
const GET_LINE_CODING: u8 = 0x21;
const SET_CONTROL_LINE_STATE: u8 = 0x22;
const CTRL_DTR: u16 = 1 << 0;
const CTRL_RTS: u16 = 1 << 1;
#[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,
}
fn scan() -> usize {
let mut n = 0;
let _ = fs::create_dir_all("/dev");
if let Ok(dir) = fs::read_dir("/scheme/usb") {
for e in dir.flatten() {
if let Ok(c) = fs::read_to_string(e.path().join("config")) {
if c.contains("class=0a") || c.contains("CDC ACM") {
let tgt = e.path();
let lnk = format!("/dev/ttyACM{}", n);
let _ = std::os::unix::fs::symlink(&tgt, &lnk);
n += 1;
}
}
}
struct AcmDevice {
handle: XhciClientHandle,
bulk_in: XhciEndpHandle,
bulk_out: XhciEndpHandle,
line_coding: LineCoding,
}
impl AcmDevice {
fn cdc_ctrl_msg(
&self, request: u8, value: u16, data: DeviceReqData,
) -> Result<(), io::Error> {
self.handle
.device_request(
PortReqTy::Class, PortReqRecipient::Interface,
request, value, 0, data,
)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("CDC: {}", e)))
}
fn set_line_coding(&mut self, lc: &LineCoding) -> Result<(), io::Error> {
let bytes = unsafe {
std::slice::from_raw_parts(lc as *const LineCoding as *const u8, 7)
};
self.cdc_ctrl_msg(SET_LINE_CODING, 0, DeviceReqData::Out(bytes))?;
self.line_coding = *lc;
Ok(())
}
fn set_control_line_state(&mut self, state: u16) -> Result<(), io::Error> {
self.cdc_ctrl_msg(SET_CONTROL_LINE_STATE, state, DeviceReqData::NoData)
}
n
}
fn main() {
log::set_logger(&StderrLogger).ok();
log::set_max_level(LevelFilter::Info);
info!("redbear-acmd: USB CDC ACM serial daemon");
loop { let c = scan(); if c > 0 { info!("redbear-acmd: {} ttyACM symlink(s)", c); } std::thread::sleep(Duration::from_secs(5)); }
log::set_max_level(log::LevelFilter::Info);
common::init();
let mut args = env::args().skip(1);
const USAGE: &str = "redbear-acmd <scheme> <port> <interface>";
let scheme = args.next().expect(USAGE);
let port_id = args.next().expect(USAGE)
.parse::<PortId>()
.expect("Expected port ID");
let interface_num = args.next().expect(USAGE)
.parse::<u8>()
.expect("Expected integer interface number");
let name = format!("{}_{}_{}_acm", scheme, port_id, interface_num);
common::setup_logging("usb", "device", &name,
common::output_level(), common::file_level());
let handle = XhciClientHandle::new(scheme.clone(), port_id)
.expect("Failed to open XhciClientHandle");
let desc: DevDesc = handle.get_standard_descs()
.expect("Failed to get descriptors");
// Find CDC ACM: control interface (class 0x02 sub 0x02) +
// data interface (class 0x0A).
let (conf_desc, _ctrl_if, data_if) = desc.config_descs.iter()
.find_map(|conf_desc| {
let data_if = conf_desc.interface_descs.iter()
.find(|ifd| ifd.class == 0x0A)?;
Some((conf_desc.clone(), conf_desc.interface_descs[0].clone(), data_if.clone()))
})
.expect("No CDC ACM data interface found");
// Configure both interfaces
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 endpoints");
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("No 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("No bulk OUT");
let mut dev = AcmDevice {
bulk_in: handle.open_endpoint(bulk_in_num)
.expect("Failed to open bulk IN"),
bulk_out: handle.open_endpoint(bulk_out_num)
.expect("Failed to open bulk OUT"),
handle,
line_coding: LineCoding { dw_dte_rate: 115200, b_data_bits: 8, ..LineCoding::default() },
};
// Set default line coding + assert DTR/RTS (Linux 7.1 acm_probe).
{
let lc = dev.line_coding;
let _ = dev.set_line_coding(&lc);
}
let _ = dev.set_control_line_state(CTRL_DTR | CTRL_RTS);
log::info!("CDC ACM ready on {} port {} — bulk_in={} bulk_out={}",
scheme, port_id, bulk_in_num, bulk_out_num);
let mut buf = [0u8; 1024];
loop {
match dev.bulk_in.transfer_read(&mut buf) {
Ok(status) if status.bytes_transferred > 0 => {
let n = status.bytes_transferred as usize;
let _ = io::stdout().write_all(&buf[..n]);
let _ = io::stdout().flush();
}
Ok(_) => {}
Err(e) => log::warn!("CDC ACM: read: {}", e),
}
thread::sleep(time::Duration::from_millis(10));
}
}