Skip to main content

netget/server/usb/serial/
handler.rs

1//! USB CDC ACM interface handler.
2//!
3//! `usbip` 0.9 ships `cdc::UsbCdcAcmHandler`, but it is a demo: a host write on the bulk OUT
4//! endpoint is logged and thrown away, and the class-specific control requests that make a
5//! serial port a serial port (`SET_LINE_CODING`, `GET_LINE_CODING`,
6//! `SET_CONTROL_LINE_STATE`) are not handled at all. Both matter here โ€” without the first,
7//! `usb_serial_data_received` can never fire; without the second, a host that opens the port
8//! and configures it gets no answer.
9//!
10//! So the CDC *descriptors* and the endpoint layout are taken from the crate (it is the
11//! authority on what a CDC ACM interface looks like on the wire) while the URB handling is
12//! implemented here.
13
14#[cfg(all(unix, feature = "libusb"))]
15use crate::server::usb::descriptors::{ControlLineState, LineCoding};
16#[cfg(all(unix, feature = "libusb"))]
17use tokio::sync::mpsc;
18#[cfg(all(unix, feature = "libusb"))]
19use tracing::{debug, trace, warn};
20
21/// CDC class-specific requests (CDC PSTN 1.2, table 13).
22#[cfg(all(unix, feature = "libusb"))]
23mod cdc_request {
24    pub const SET_LINE_CODING: u8 = 0x20;
25    pub const GET_LINE_CODING: u8 = 0x21;
26    pub const SET_CONTROL_LINE_STATE: u8 = 0x22;
27    pub const SEND_BREAK: u8 = 0x23;
28}
29
30/// `bmRequestType` values for class requests addressed to an interface.
31#[cfg(all(unix, feature = "libusb"))]
32mod request_type {
33    /// Host-to-device, class, interface.
34    pub const CLASS_INTERFACE_OUT: u8 = 0b0010_0001;
35    /// Device-to-host, class, interface.
36    pub const CLASS_INTERFACE_IN: u8 = 0b1010_0001;
37}
38
39/// CDC class-specific notifications (CDC PSTN 1.2, table 30), sent on the interrupt IN endpoint.
40#[cfg(all(unix, feature = "libusb"))]
41mod cdc_notification {
42    /// `bmRequestType` for a device-to-host class notification about an interface.
43    pub const REQUEST_TYPE: u8 = 0b1010_0001;
44    /// `SERIAL_STATE` (CDC PSTN 1.2 ยง6.5.4): the UART state bitmap.
45    pub const SERIAL_STATE: u8 = 0x20;
46
47    /// `bOverRun` (D6) โ€” "received data has been discarded due to overrun in the device".
48    pub const UART_STATE_OVERRUN: u16 = 1 << 6;
49}
50
51/// Most device-to-host bytes held waiting for the host's bulk IN URBs.
52///
53/// The model fills this buffer and the host drains it, so with no cap the model's pace decides
54/// how much memory a port holds and a host that never polls never gives any of it back.
55/// 64 KiB is several seconds of traffic at 115200 baud, the port's own default line rate.
56#[cfg(all(unix, feature = "libusb"))]
57const MAX_TX_BUFFER: usize = 64 * 1024;
58
59/// A virtual CDC ACM serial port.
60///
61/// `Debug` is required by `usbip::UsbInterfaceHandler` as of 0.9. It is derived: the only
62/// buffered content is bytes the LLM asked to send, which are already logged at TRACE.
63#[cfg(all(unix, feature = "libusb"))]
64#[derive(Debug)]
65pub struct UsbCdcAcmSerialHandler {
66    /// Device-to-host bytes, drained by bulk IN URBs.
67    tx_buffer: Vec<u8>,
68    /// Host-to-device bytes. Handed to the connection task, which raises
69    /// `usb_serial_data_received`; `handle_urb` is sync and cannot call the LLM itself.
70    rx_tx: mpsc::UnboundedSender<Vec<u8>>,
71    /// Line parameters the port reports back on `GET_LINE_CODING`.
72    line_coding: LineCoding,
73    /// DTR/RTS as last set by the host.
74    control_lines: ControlLineState,
75    /// Whole CDC notifications waiting for the host's next interrupt IN URB.
76    ///
77    /// Queued as messages rather than as a byte stream: a host parses one notification per
78    /// transfer, so two must never be merged into one URB.
79    notifications: std::collections::VecDeque<Vec<u8>>,
80}
81
82#[cfg(all(unix, feature = "libusb"))]
83impl UsbCdcAcmSerialHandler {
84    pub fn new(rx_tx: mpsc::UnboundedSender<Vec<u8>>) -> Self {
85        Self {
86            tx_buffer: Vec::new(),
87            rx_tx,
88            line_coding: LineCoding::default_115200_8n1(),
89            control_lines: ControlLineState::from_value(0),
90            notifications: std::collections::VecDeque::new(),
91        }
92    }
93
94    /// Tell the host its bytes were dropped, in CDC's own vocabulary.
95    ///
96    /// A serial port has no request/response framing, so there is no reply to fail โ€” but CDC
97    /// PSTN 1.2 does define what a device says when it could not take what the host wrote:
98    /// `SERIAL_STATE` with `bOverRun` set, on the interrupt IN (notification) endpoint. That is
99    /// what this queues. A Linux `cdc-acm` host counts it as a receive overrun on the tty.
100    ///
101    /// Silence would be the alternative and it is the wrong one: a port that says nothing is
102    /// indistinguishable from a port with nothing to say, so an LLM outage would look exactly
103    /// like a quiet peer.
104    pub fn queue_serial_state_overrun(&mut self) {
105        let state = cdc_notification::UART_STATE_OVERRUN;
106        // bmRequestType, bNotification, wValue, wIndex (interface 0), wLength, then the bitmap.
107        let mut message = Vec::with_capacity(10);
108        message.push(cdc_notification::REQUEST_TYPE);
109        message.push(cdc_notification::SERIAL_STATE);
110        message.extend_from_slice(&0u16.to_le_bytes()); // wValue
111        message.extend_from_slice(&0u16.to_le_bytes()); // wIndex: the comms interface
112        message.extend_from_slice(&2u16.to_le_bytes()); // wLength
113        message.extend_from_slice(&state.to_le_bytes());
114        debug!("USB serial queueing SERIAL_STATE notification with bOverRun set");
115        self.notifications.push_back(message);
116    }
117
118    /// Notifications still waiting to go out. Used by tests and diagnostics.
119    #[allow(dead_code)]
120    pub fn pending_notifications(&self) -> usize {
121        self.notifications.len()
122    }
123
124    /// The endpoints a CDC ACM interface needs, taken verbatim from the `usbip` crate:
125    /// interrupt IN for notifications, bulk IN and bulk OUT for data.
126    pub fn endpoints() -> Vec<usbip::UsbEndpoint> {
127        usbip::cdc::UsbCdcAcmHandler::endpoints()
128    }
129
130    /// Queue bytes for the host to read on the next bulk IN URB.
131    ///
132    /// Bounded: the host drains this one bulk IN URB at a time and may never poll at all, while
133    /// `send_data` can be called on every model turn. An unbounded buffer therefore grows at
134    /// the model's pace and shrinks at the host's, which is the wrong way round. A real UART
135    /// drops on a full transmit buffer and reports it, so that is what this does -- the drop is
136    /// returned rather than swallowed so the caller can tell the model its bytes did not go.
137    pub fn queue_tx(&mut self, data: &[u8]) -> usize {
138        let room = MAX_TX_BUFFER.saturating_sub(self.tx_buffer.len());
139        let taken = data.len().min(room);
140        self.tx_buffer.extend_from_slice(&data[..taken]);
141        let dropped = data.len() - taken;
142        if dropped > 0 {
143            warn!(
144                "USB serial transmit buffer is full at {} byte(s); dropped {} of {} queued",
145                self.tx_buffer.len(),
146                dropped,
147                data.len()
148            );
149        } else {
150            trace!("USB serial queue {} byte(s) for the host", data.len());
151        }
152        dropped
153    }
154
155    /// Replace the reported line parameters.
156    pub fn set_line_coding(&mut self, line_coding: LineCoding) {
157        debug!(
158            "USB serial line coding set by handler: {} baud, {} data bits, parity {}, stop {}",
159            line_coding.baud_rate, line_coding.data_bits, line_coding.parity, line_coding.stop_bits
160        );
161        self.line_coding = line_coding;
162    }
163
164    /// The line parameters currently reported to the host.
165    pub fn line_coding(&self) -> LineCoding {
166        self.line_coding
167    }
168
169    /// Bytes still waiting to go out. Used by tests and diagnostics.
170    #[allow(dead_code)]
171    pub fn pending_tx(&self) -> usize {
172        self.tx_buffer.len()
173    }
174
175    /// Handle a class-specific control transfer addressed to this interface.
176    fn handle_control(
177        &mut self,
178        setup: usbip::SetupPacket,
179        req: &[u8],
180    ) -> std::result::Result<Vec<u8>, std::io::Error> {
181        match (setup.request_type, setup.request) {
182            (request_type::CLASS_INTERFACE_OUT, cdc_request::SET_LINE_CODING) => {
183                // The length check lives in `LineCoding::from_bytes`, which is `pub` and takes
184                // an arbitrary slice; duplicating it here is how the unchecked indexing it used
185                // to do looked safe.
186                match LineCoding::from_bytes(req) {
187                    Some(coding) => {
188                        self.line_coding = coding;
189                        debug!(
190                            "USB serial host set line coding: {} baud, {} data bits, parity {}, \
191                             stop {}",
192                            self.line_coding.baud_rate,
193                            self.line_coding.data_bits,
194                            self.line_coding.parity,
195                            self.line_coding.stop_bits
196                        );
197                    }
198                    None => warn!(
199                        "USB serial SET_LINE_CODING carried {} bytes, expected {} - ignored",
200                        req.len(),
201                        LineCoding::WIRE_LEN
202                    ),
203                }
204                // A control OUT transfer must answer with an empty buffer; usbip's own
205                // `debug_assert` in `usbip_ret_submit_success` enforces it.
206                Ok(vec![])
207            }
208            (request_type::CLASS_INTERFACE_IN, cdc_request::GET_LINE_CODING) => {
209                Ok(self.line_coding.to_bytes().to_vec())
210            }
211            (request_type::CLASS_INTERFACE_OUT, cdc_request::SET_CONTROL_LINE_STATE) => {
212                self.control_lines = ControlLineState::from_value(setup.value);
213                debug!(
214                    "USB serial host set control lines: DTR={} RTS={}",
215                    self.control_lines.dtr, self.control_lines.rts
216                );
217                Ok(vec![])
218            }
219            (request_type::CLASS_INTERFACE_OUT, cdc_request::SEND_BREAK) => {
220                debug!("USB serial host sent a break of {} ms", setup.value);
221                Ok(vec![])
222            }
223            _ => {
224                // Answer unknown control requests empty rather than with an error: an error
225                // aborts the USB/IP session for the whole device, and a host probing an
226                // optional request must not unplug the port.
227                debug!(
228                    "USB serial: unhandled control request type={:#04x} request={:#04x}",
229                    setup.request_type, setup.request
230                );
231                Ok(vec![])
232            }
233        }
234    }
235}
236
237#[cfg(all(unix, feature = "libusb"))]
238impl usbip::UsbInterfaceHandler for UsbCdcAcmSerialHandler {
239    fn handle_urb(
240        &mut self,
241        _interface: &usbip::UsbInterface,
242        ep: usbip::UsbEndpoint,
243        transfer_buffer_length: u32,
244        setup: usbip::SetupPacket,
245        req: &[u8],
246    ) -> std::result::Result<Vec<u8>, std::io::Error> {
247        if ep.is_ep0() {
248            return self.handle_control(setup, req);
249        }
250
251        match ep.direction() {
252            usbip::Direction::Out => {
253                // Bulk OUT: the host wrote to the port. This is the path the stock
254                // `UsbCdcAcmHandler` drops on the floor.
255                if !req.is_empty() {
256                    trace!("USB serial received {} byte(s) from the host", req.len());
257                    if self.rx_tx.send(req.to_vec()).is_err() {
258                        // The connection task is gone, so nothing will ever raise an event
259                        // for these bytes. Say so rather than silently discarding them.
260                        warn!(
261                            "USB serial dropped {} byte(s): connection task has exited",
262                            req.len()
263                        );
264                    }
265                }
266                Ok(vec![])
267            }
268            usbip::Direction::In => {
269                if ep.attributes == usbip::EndpointAttributes::Interrupt as u8 {
270                    // The notification endpoint. One whole message per transfer; a message
271                    // longer than the URB is split and the remainder pushed back to the front,
272                    // so it reassembles without ever being merged with the next one.
273                    let limit = (ep.max_packet_size as usize).min(transfer_buffer_length as usize);
274                    let Some(mut message) = self.notifications.pop_front() else {
275                        return Ok(vec![]);
276                    };
277                    if message.len() > limit {
278                        let remainder = message.split_off(limit);
279                        self.notifications.push_front(remainder);
280                    }
281                    trace!(
282                        "USB serial sent a {} byte CDC notification to the host",
283                        message.len()
284                    );
285                    return Ok(message);
286                }
287
288                // Bulk IN: hand the host as much as this URB can take.
289                let limit = (ep.max_packet_size as usize).min(transfer_buffer_length as usize);
290                let take = self.tx_buffer.len().min(limit);
291                let resp: Vec<u8> = self.tx_buffer.drain(..take).collect();
292                if !resp.is_empty() {
293                    trace!("USB serial sent {} byte(s) to the host", resp.len());
294                }
295                Ok(resp)
296            }
297        }
298    }
299
300    fn get_class_specific_descriptor(&self) -> Vec<u8> {
301        // The CDC header and ACM functional descriptors, as built by the usbip crate.
302        usbip::UsbInterfaceHandler::get_class_specific_descriptor(
303            &usbip::cdc::UsbCdcAcmHandler::new(),
304        )
305    }
306
307    fn as_any(&mut self) -> &mut dyn std::any::Any {
308        self
309    }
310}