USB: P3 scheme service wrapper — redbear-acmd registers /scheme/ttys/usbACM_<N>

Cross-referenced with Linux 7.1 tty_port_register_device() pattern.

New scheme.rs module:
- AcmScheme implements SchemeSync with Mutex-wrapped XhciEndpHandle
- openat(): root dir listing + device file open (O_RDWR)
- read(): USB bulk IN → scheme client (getty, terminal)
- write(): scheme client → USB bulk OUT
- close(): decrements open count
- fsync(): no-op

main.rs:
- #[cfg(target_os = redox)]: Socket::create() + register scheme
  under /scheme/ttys/usbACM_<port_id>, process requests with
  handle_scheme_mut() in event loop
- #[cfg(not(target_os = redox))]: stdout fallback for testing

This enables getty to open /scheme/ttys/usbACM_<N> as a serial
console on USB CDC ACM devices — the driver is now a proper
Redox scheme service, not just a stdout debugging tool.

Pattern replicable for redbear-ftdi (same scheme:ttys), redbear-ecmd
(scheme:net), and redbear-usbaudiod (scheme:audio).
This commit is contained in:
2026-07-07 19:21:14 +03:00
parent 0dc1786f66
commit 726e628e0d
3 changed files with 143 additions and 85 deletions
@@ -10,6 +10,7 @@ path = "src/main.rs"
[dependencies]
log = "0.4"
redox_syscall = { path = "../../../../../local/sources/syscall" }
redox-scheme = { path = "../../../../../local/sources/redox-scheme", package = "redox-scheme" }
xhcid = { path = "../../../../../local/sources/base/drivers/usb/xhcid" }
common = { path = "../../../../../local/sources/base/drivers/common" }
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
@@ -7,12 +7,11 @@
//! On Redox: registers a `/scheme/ttys/usbACM_<N>` scheme that getty
//! and other terminal programs can open as a serial console.
//! On host/Linux: writes to stdout for testing.
//!
//! 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::{env, io, io::{Read, Write}, thread, time};
#[cfg(target_os = "redox")]
mod scheme;
use std::{env, io, io::Write, thread, time};
use xhcid_interface::{
ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpDirection, EndpointTy,
@@ -22,18 +21,12 @@ use xhcid_interface::{
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,
}
struct LineCoding { dw_dte_rate: u32, b_char_format: u8, b_parity_type: u8, b_data_bits: u8 }
struct AcmDevice {
handle: XhciClientHandle,
@@ -48,14 +41,12 @@ impl AcmDevice {
.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)
}
@@ -64,104 +55,64 @@ impl AcmDevice {
fn main() {
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 interface number");
let scheme = args.next().expect("redbear-acmd <scheme> <port> <iface>");
let port_id = args.next().expect("redbear-acmd <scheme> <port> <iface>").parse::<PortId>().expect("port");
let _ = args.next().expect("redbear-acmd <scheme> <port> <iface>").parse::<u8>().expect("iface");
let name = format!("{}_{}_acm", scheme, port_id);
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");
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(|conf_desc| {
let data_if = conf_desc.interface_descs.iter().find(|ifd| ifd.class == 0x0A)?;
Some((conf_desc.clone(), data_if.clone()))
})
.expect("No CDC ACM data interface found");
.find_map(|c| c.interface_descs.iter().find(|i| i.class == 0x0A).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 endpoints");
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("No bulk IN");
.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("No bulk OUT");
.map(|i| (i + 1) as u8).expect("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() },
bulk_in: handle.open_endpoint(bulk_in_num).expect("open IN"),
bulk_out: handle.open_endpoint(bulk_out_num).expect("open OUT"),
handle, line_coding: LineCoding { dw_dte_rate: 115200, b_data_bits: 8, ..LineCoding::default() },
};
let lc = dev.line_coding;
let _ = dev.set_line_coding(&lc);
let _ = dev.set_control_line_state(CTRL_DTR | CTRL_RTS);
let scheme_name = format!("ttys/usbACM_{}", port_id);
log::info!("CDC ACM ready on {} port {} — scheme=/{}, bulk_in={} bulk_out={}",
scheme, port_id, scheme_name, bulk_in_num, bulk_out_num);
// On Redox: register a scheme so getty can use this as a serial console.
// On host/Linux: fall back to stdout for testing.
// ── Scheme IPC path (Redox) ──
#[cfg(target_os = "redox")]
{
use redox_scheme::Socket;
let socket = Socket::create().expect("CDC ACM: failed to create scheme socket");
let _ = socket.register(&scheme_name).expect("CDC ACM: failed to register scheme");
log::info!("CDC ACM: scheme registered at /scheme/{}", scheme_name);
// Accept connections and relay USB → scheme, scheme → USB.
let mut buf = [0u8; 1024];
use redox_scheme::scheme::SchemeSync;
use redox_scheme::{Socket, SignalBehavior};
let scheme_name = format!("ttys/usbACM_{}", port_id);
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);
socket.register(&scheme_name).expect("CDC ACM: scheme register");
log::info!("CDC ACM: scheme /scheme/{} ready", scheme_name);
loop {
let fd = match socket.accept() {
Ok(fd) => fd,
Err(e) => { log::warn!("CDC ACM: accept: {}", e); continue; }
};
log::info!("CDC ACM: client connected to /scheme/{}", scheme_name);
let mut client = unsafe { std::fs::File::from_raw_fd(fd.raw()) };
loop {
// USB→client
match dev.bulk_in.transfer_read(&mut buf) {
Ok(s) if s.bytes_transferred > 0 => {
let _ = client.write_all(&buf[..s.bytes_transferred as usize]);
}
Ok(_) => {}
Err(e) => { log::warn!("CDC ACM: read: {}", e); break; }
}
// client→USB (non-blocking)
match client.read(&mut buf) {
Ok(0) => { log::info!("CDC ACM: client disconnected"); break; }
Ok(n) => { let _ = dev.bulk_out.transfer_write(&buf[..n]); }
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {}
Err(e) => { log::warn!("CDC ACM: client read: {}", e); break; }
}
thread::sleep(time::Duration::from_millis(10));
let req = socket.next_request(SignalBehavior::Restart).expect("CDC ACM: scheme request");
if let Err(e) = req.handle_scheme_mut(&mut acm_scheme, &mut scheme_state) {
log::warn!("CDC ACM: scheme error: {}", e);
}
}
}
// ── Stdout path (host/Linux testing) ──
#[cfg(not(target_os = "redox"))]
{
log::info!("CDC ACM: stdout mode — scheme={} 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(s) if s.bytes_transferred > 0 => { let _ = io::stdout().write_all(&buf[..s.bytes_transferred as usize]); let _ = io::stdout().flush(); }
Ok(_) => {}
Err(e) => log::warn!("CDC ACM: read: {}", e),
}
@@ -0,0 +1,106 @@
use std::io::{self, Read, Write};
use std::sync::Mutex;
use std::time::Duration;
use redox_scheme::scheme::SchemeSync;
use redox_scheme::{CallerCtx, OpenResult};
use syscall::error::{Error, Result, EBADF, EINVAL};
use syscall::flag::{MODE_FILE, O_RDWR, O_STAT};
use syscall::Stat;
use xhcid_interface::XhciEndpHandle;
pub struct AcmScheme {
bulk_in: Mutex<XhciEndpHandle>,
bulk_out: Mutex<XhciEndpHandle>,
open_count: Mutex<usize>,
}
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),
}
}
}
impl SchemeSync for AcmScheme {
fn openat(
&self,
id: usize,
path: &str,
_flags: usize,
_ctx: &CallerCtx,
) -> Result<OpenResult> {
if id != 1 {
return Err(Error::new(EBADF));
}
if path.is_empty() || path == "." || path == "/" {
// Root directory listing
if path.is_empty() {
*self.open_count.lock().unwrap_or_else(|e| e.into_inner()) += 1;
return Ok(OpenResult::ThisScheme {
number: 1,
flags: O_STAT | MODE_FILE,
});
}
return Err(Error::new(EINVAL));
}
// Opening the device (path == "tty" or "" on open)
let mut count = self.open_count.lock().unwrap_or_else(|e| e.into_inner());
let next_id = 2 + *count;
*count += 1;
Ok(OpenResult::OtherScheme {
number: next_id,
flags: O_RDWR,
})
}
fn read(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
if id < 2 || buf.is_empty() {
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)
}
Ok(_) => Ok(0),
Err(e) => {
log::warn!("ACM scheme: read: {}", e);
Err(Error::new(EINVAL))
}
}
}
fn write(&mut self, id: usize, buf: &[u8], _ctx: &CallerCtx) -> Result<usize> {
if id < 2 || 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)
}
Ok(_) => Ok(0),
Err(e) => {
log::warn!("ACM scheme: write: {}", e);
Err(Error::new(EINVAL))
}
}
}
fn fsync(&mut self, _id: usize, _ctx: &CallerCtx) -> Result<()> {
Ok(())
}
fn close(&self, id: usize, _ctx: &CallerCtx) -> Result<()> {
if id >= 2 {
let mut count = self.open_count.lock().unwrap_or_else(|e| e.into_inner());
*count = count.saturating_sub(1);
}
Ok(())
}
}