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}