1#[cfg(all(unix, feature = "libusb"))]
4use crate::llm::actions::{
5 protocol_trait::{ActionResult, Protocol, Server},
6 ActionDefinition, Parameter,
7};
8#[cfg(all(unix, feature = "libusb"))]
9use crate::protocol::log_template::LogTemplate;
10#[cfg(all(unix, feature = "libusb"))]
11use crate::{protocol::EventType, server::connection::ConnectionId, state::app_state::AppState};
12#[cfg(all(unix, feature = "libusb"))]
13use anyhow::{Context, Result};
14#[cfg(all(unix, feature = "libusb"))]
15use serde_json::json;
16#[cfg(all(unix, feature = "libusb"))]
17use std::{
18 collections::HashMap,
19 sync::{Arc, LazyLock},
20};
21
22#[cfg(all(unix, feature = "libusb"))]
32fn connection_id_parameter() -> Parameter {
33 Parameter {
34 name: "connection_id".to_string(),
35 type_hint: "string".to_string(),
39 description: "Which attached port to act on, as the event reports it (e.g. \"conn-7\"). \
40 Omit it when only one host is attached; required (copy it from the event) when \
41 there are several."
42 .to_string(),
43 required: false,
44 }
45}
46
47#[cfg(all(unix, feature = "libusb"))]
48fn send_data_action() -> ActionDefinition {
49 ActionDefinition {
50 name: "send_data".to_string(),
51 description: "Send data to the host over the virtual serial port, as if a device had \
52 written it to the wire."
53 .to_string(),
54 parameters: vec![
55 Parameter {
56 name: "data".to_string(),
57 type_hint: "string".to_string(),
58 description: "Text to send. Include an explicit \"\\n\" or \"\\r\\n\" if the host \
59 expects line-terminated output"
60 .to_string(),
61 required: true,
62 },
63 connection_id_parameter(),
64 ],
65 example: json!({"type": "send_data", "data": "Hello\n"}),
66 log_template: Some(
67 LogTemplate::new()
68 .with_info("-> USB serial send {data_len}B")
69 .with_debug("USB-Serial send_data: data='{data}'"),
70 ),
71 }
72}
73
74#[cfg(all(unix, feature = "libusb"))]
75fn set_line_coding_action() -> ActionDefinition {
76 ActionDefinition {
77 name: "set_line_coding".to_string(),
78 description: "Set the baud rate and line parameters this virtual port reports. Defaults \
79 to 115200 8N1."
80 .to_string(),
81 parameters: vec![
82 Parameter {
83 name: "baud_rate".to_string(),
84 type_hint: "number".to_string(),
85 description: "Bits per second (e.g., 115200)".to_string(),
86 required: true,
87 },
88 Parameter {
89 name: "data_bits".to_string(),
90 type_hint: "number".to_string(),
91 description: "5, 6, 7, 8, or 16".to_string(),
92 required: false,
93 },
94 Parameter {
95 name: "parity".to_string(),
96 type_hint: "string".to_string(),
97 description: "'none', 'odd', 'even', 'mark', 'space'".to_string(),
98 required: false,
99 },
100 Parameter {
101 name: "stop_bits".to_string(),
102 type_hint: "number".to_string(),
103 description: "1, 1.5, or 2".to_string(),
104 required: false,
105 },
106 connection_id_parameter(),
107 ],
108 example: json!({"type": "set_line_coding", "baud_rate": 9600}),
109 log_template: Some(
110 LogTemplate::new()
111 .with_info("-> USB serial line coding {baud_rate} baud")
112 .with_debug("USB-Serial set_line_coding: baud={baud_rate} data_bits={data_bits} parity={parity} stop_bits={stop_bits}"),
113 ),
114 }
115}
116
117#[cfg(all(unix, feature = "libusb"))]
118fn wait_for_more_action() -> ActionDefinition {
119 ActionDefinition {
120 name: "wait_for_more".to_string(),
121 description: "Send nothing and wait for the host to write more. Correct whenever the \
122 input so far is incomplete - a partial line, for instance."
123 .to_string(),
124 parameters: vec![],
125 example: json!({"type": "wait_for_more"}),
126 log_template: Some(
127 LogTemplate::new()
128 .with_info("-> USB serial wait for more")
129 .with_debug("USB-Serial wait_for_more"),
130 ),
131 }
132}
133
134#[cfg(all(unix, feature = "libusb"))]
135pub static USB_SERIAL_ATTACHED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
136 EventType::new(
137 "usb_serial_attached",
138 "A host opened this virtual serial port (it appears as /dev/ttyACM0 on Linux). Send a \
139 banner or prompt if the device should greet the host, adjust the line coding, or \
140 wait_for_more to stay silent until the host writes.",
141 json!({"type": "send_data", "data": "READY\r\n"}),
142 )
143 .with_parameters(vec![Parameter {
144 name: "connection_id".to_string(),
145 type_hint: "string".to_string(),
146 description: "Connection ID".to_string(),
147 required: true,
148 }])
149 .with_actions(vec![
150 send_data_action(),
151 set_line_coding_action(),
152 wait_for_more_action(),
153 ])
154 .with_alternative_example(json!({"type": "wait_for_more"}))
155});
156
157#[cfg(all(unix, feature = "libusb"))]
158pub static USB_SERIAL_DETACHED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
159 EventType::new(
160 "usb_serial_detached",
161 "The host closed this virtual serial port. Purely informational - the port is gone, so \
162 written data has nowhere to go.",
163 json!({"type": "show_message", "message": "USB serial host detached"}),
164 )
165 .with_parameters(vec![Parameter {
166 name: "connection_id".to_string(),
167 type_hint: "string".to_string(),
168 description: "Connection ID".to_string(),
169 required: true,
170 }])
171 .with_no_actions()
172});
173
174#[cfg(all(unix, feature = "libusb"))]
175pub static USB_SERIAL_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
176 EventType::new(
177 "usb_serial_data_received",
178 "The host wrote data to the virtual serial port. Reply with send_data, or wait_for_more \
179 if what arrived is only part of a command.",
180 json!({"type": "send_data", "data": "OK\r\n"}),
181 )
182 .with_parameters(vec![
183 Parameter {
184 name: "connection_id".to_string(),
185 type_hint: "string".to_string(),
186 description: "Connection ID".to_string(),
187 required: true,
188 },
189 Parameter {
190 name: "data".to_string(),
191 type_hint: "string".to_string(),
192 description: "Received data as string".to_string(),
193 required: true,
194 },
195 ])
196 .with_actions(vec![
197 send_data_action(),
198 set_line_coding_action(),
199 wait_for_more_action(),
200 ])
201 .with_alternative_example(json!({"type": "wait_for_more"}))
202});
203
204#[cfg(all(unix, feature = "libusb"))]
210type SharedHandler = Arc<std::sync::Mutex<Box<dyn usbip::UsbInterfaceHandler + Send>>>;
211
212#[cfg(all(unix, feature = "libusb"))]
213pub struct UsbSerialProtocol {
214 handlers: Arc<std::sync::Mutex<HashMap<ConnectionId, SharedHandler>>>,
215}
216
217#[cfg(all(unix, feature = "libusb"))]
218impl UsbSerialProtocol {
219 pub fn new() -> Self {
220 Self {
221 handlers: Arc::new(std::sync::Mutex::new(HashMap::new())),
222 }
223 }
224
225 pub fn set_handler(&self, connection_id: ConnectionId, handler: SharedHandler) {
227 if let Ok(mut handlers) = self.handlers.lock() {
228 handlers.insert(connection_id, handler);
229 }
230 }
231
232 pub fn remove_handler(&self, connection_id: ConnectionId) {
235 if let Ok(mut handlers) = self.handlers.lock() {
236 handlers.remove(&connection_id);
237 }
238 }
239
240 fn resolve_handler(&self, action: &serde_json::Value) -> Result<SharedHandler> {
246 let handlers = self
247 .handlers
248 .lock()
249 .map_err(|_| anyhow::anyhow!("USB serial handler registry poisoned"))?;
250
251 let requested = action["connection_id"]
260 .as_u64()
261 .and_then(|id| u32::try_from(id).ok())
262 .map(ConnectionId::new)
263 .or_else(|| {
264 action["connection_id"]
265 .as_str()
266 .and_then(ConnectionId::from_string)
267 });
268
269 if let Some(connection_id) = requested {
270 return handlers.get(&connection_id).cloned().ok_or_else(|| {
271 anyhow::anyhow!(
272 "No USB serial port attached on connection {}",
273 connection_id
274 )
275 });
276 }
277
278 match handlers.len() {
279 0 => Err(anyhow::anyhow!(
280 "No USB serial port is attached, so there is nothing to write to"
281 )),
282 1 => Ok(handlers.values().next().cloned().expect("len checked")),
283 _ => {
284 let mut ids: Vec<u32> = handlers.keys().map(|c| c.as_u32()).collect();
285 ids.sort_unstable();
286 Err(anyhow::anyhow!(
287 "{} USB serial ports are attached ({:?}); the action must name one with \
288 'connection_id'",
289 ids.len(),
290 ids
291 ))
292 }
293 }
294 }
295
296 pub fn signal_overrun(&self, connection_id: ConnectionId) -> bool {
301 let handler = match self.handlers.lock() {
302 Ok(handlers) => handlers.get(&connection_id).cloned(),
303 Err(_) => None,
304 };
305 let Some(handler) = handler else {
306 return false;
307 };
308 let Ok(mut guard) = handler.lock() else {
309 return false;
310 };
311 match guard
312 .as_any()
313 .downcast_mut::<crate::server::usb::serial::handler::UsbCdcAcmSerialHandler>()
314 {
315 Some(serial) => {
316 serial.queue_serial_state_overrun();
317 true
318 }
319 None => false,
320 }
321 }
322
323 fn with_serial_handler<T>(
325 &self,
326 action: &serde_json::Value,
327 f: impl FnOnce(&mut crate::server::usb::serial::handler::UsbCdcAcmSerialHandler) -> T,
328 ) -> Result<T> {
329 let handler = self.resolve_handler(action)?;
330 let mut guard = handler
331 .lock()
332 .map_err(|_| anyhow::anyhow!("USB serial handler mutex poisoned"))?;
333 let serial = guard
334 .as_any()
335 .downcast_mut::<crate::server::usb::serial::handler::UsbCdcAcmSerialHandler>()
336 .context("Handler is not a USB CDC ACM serial handler")?;
337 Ok(f(serial))
338 }
339}
340
341#[cfg(all(unix, feature = "libusb"))]
342impl Protocol for UsbSerialProtocol {
343 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
344 vec![
345 crate::llm::actions::ParameterDefinition {
346 name: "first_byte_timeout_secs".to_string(),
347 type_hint: "number".to_string(),
348 description: "Seconds a peer that has only connected may send no USB/IP message \
349 at all before the server closes it. Default 30. USB/IP is \
350 client-speaks-first and nothing here answers a peer that has not \
351 asked - `usbip attach` and `usbip list -r` both write their \
352 request as soon as the socket is up - so a short bound is the \
353 honest one. Raise it only if your host is reached over a link \
354 where a handshake can take that long."
355 .to_string(),
356 required: false,
357 example: serde_json::json!(30),
358 default: Some(serde_json::json!(
359 crate::server::usb::guard::DEFAULT_FIRST_MESSAGE_TIMEOUT.as_secs()
360 )),
361 },
362 crate::llm::actions::ParameterDefinition {
363 name: "idle_timeout_secs".to_string(),
364 type_hint: "number".to_string(),
365 description: "Seconds an attached host may go without sending a further USB/IP \
366 message before the server closes the session. Default 1800. \
367 USB/IP has no keepalive and nothing obliges an attached host to \
368 speak - a serial port a host has imported and is asking nothing of \
369 is idle by design - so this is deliberately long and exists to \
370 reap a peer that is gone rather than to police one that is here. \
371 Lower it if you are exporting to strangers."
372 .to_string(),
373 required: false,
374 example: serde_json::json!(1800),
375 default: Some(serde_json::json!(
376 crate::server::usb::guard::DEFAULT_IDLE_TIMEOUT.as_secs()
377 )),
378 },
379 ]
380 }
381 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
382 vec![]
383 }
384 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
385 vec![
386 send_data_action(),
387 set_line_coding_action(),
388 wait_for_more_action(),
389 ]
390 }
391 fn protocol_name(&self) -> &'static str {
392 "USB-Serial"
393 }
394 fn get_event_types(&self) -> Vec<EventType> {
395 vec![
396 USB_SERIAL_ATTACHED_EVENT.clone(),
397 USB_SERIAL_DETACHED_EVENT.clone(),
398 USB_SERIAL_DATA_RECEIVED_EVENT.clone(),
399 ]
400 }
401 fn stack_name(&self) -> &'static str {
402 "USB>CDC>ACM"
403 }
404 fn keywords(&self) -> Vec<&'static str> {
405 vec!["usb", "serial", "cdc", "acm", "uart", "tty"]
406 }
407 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
408 crate::protocol::metadata::ProtocolMetadataV2::builder()
409 .state(crate::protocol::metadata::DevelopmentState::Experimental)
410 .implementation(
411 "Virtual USB CDC ACM serial port over USB/IP. usbip::handler runs on the \
412 accepted socket, so the listen port is whatever the caller asks for. The CDC \
413 interface handler is hand-written (src/server/usb/serial/handler.rs) because \
414 usbip's own UsbCdcAcmHandler discards host writes and handles no class \
415 requests; the CDC functional descriptors and endpoint layout still come from \
416 the crate.",
417 )
418 .llm_control(
419 "send_data queues bytes for the host's next bulk IN transfer; set_line_coding \
420 changes what GET_LINE_CODING reports. All three events are emitted: \
421 usb_serial_attached on connect, usb_serial_data_received on every host write, \
422 usb_serial_detached when the USB/IP session ends.",
423 )
424 .e2e_testing(
425 "E2E drives a real USB/IP client over TCP: OP_REQ_IMPORT, then bulk OUT and \
426 bulk IN URBs, asserting the bytes the host would read. No usbip kernel module \
427 or root is needed.",
428 )
429 .privilege_requirement(crate::protocol::metadata::PrivilegeRequirement::None)
430 .notes(
431 "Attaching from a real Linux host still needs the vhci-hcd module and root on \
432 the client side; that path is untested. SET_LINE_CODING from the host is \
433 recorded but raises no event, so a handler cannot react to the host changing \
434 the baud rate. There is no flow control and no serial-state notification on \
435 the interrupt endpoint (break, DCD, framing errors are never reported).",
436 )
437 .max_inbound_bytes(crate::server::usb::guard::MAX_TRANSFER_BUFFER_BYTES)
440 .build()
441 }
442 fn description(&self) -> &'static str {
443 "Virtual USB serial port (CDC ACM)"
444 }
445 fn example_prompt(&self) -> &'static str {
446 "Create a USB serial port and echo back any data received"
447 }
448 fn group_name(&self) -> &'static str {
449 "USB Devices"
450 }
451
452 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
453 use crate::llm::actions::StartupExamples;
454 use serde_json::json;
455
456 let script = r#"import json, sys
459data = json.load(sys.stdin)
460event = data["event"]
461if data["event_type_id"] == "usb_serial_data_received":
462 actions = [{"type": "send_data", "data": str(event.get("data", ""))}]
463else:
464 actions = []
465print(json.dumps({"actions": actions}))"#;
466
467 StartupExamples::new(
468 json!({
470 "type": "open_server",
471 "port": 3240,
472 "base_stack": "usb-serial",
473 "instruction": "Create a USB serial port and echo back any data received"
474 }),
475 json!({
477 "type": "open_server",
478 "port": 3240,
479 "base_stack": "usb-serial",
480 "event_handlers": [{
481 "event_pattern": "usb_serial_data_received",
482 "handler": {
483 "type": "script",
484 "language": "python",
485 "code": script
486 }
487 }]
488 }),
489 json!({
491 "type": "open_server",
492 "port": 3240,
493 "base_stack": "usb-serial",
494 "event_handlers": [{
495 "event_pattern": "usb_serial_attached",
496 "handler": {
497 "type": "static",
498 "actions": [{
499 "type": "send_data",
500 "data": "Welcome to NetGet USB Serial!\r\n"
501 }]
502 }
503 }]
504 }),
505 )
506 }
507}
508
509#[cfg(all(unix, feature = "libusb"))]
510impl Server for UsbSerialProtocol {
511 fn spawn(
512 &self,
513 ctx: crate::protocol::SpawnContext,
514 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
515 {
516 Box::pin(async move {
517 let secs = |name: &str| -> anyhow::Result<Option<u64>> {
523 Ok(ctx
524 .startup_params
525 .as_ref()
526 .map(|p| p.get_optional_u64(name))
527 .transpose()?
528 .flatten())
529 };
530 let first_byte_timeout_secs = secs("first_byte_timeout_secs")?;
531 let idle_timeout_secs = secs("idle_timeout_secs")?;
532 crate::server::usb::serial::UsbSerialServer::spawn_with_llm_actions(
533 ctx.legacy_listen_addr(),
534 ctx.llm_client,
535 ctx.state,
536 ctx.status_tx,
537 ctx.server_id,
538 first_byte_timeout_secs,
539 idle_timeout_secs,
540 )
541 .await
542 })
543 }
544
545 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
546 let action_type = action["type"]
547 .as_str()
548 .context("Action must have 'type' field")?;
549 match action_type {
550 "send_data" => {
551 let data = action["data"]
552 .as_str()
553 .context("send_data requires 'data' field")?;
554 if data.is_empty() {
555 return Err(anyhow::anyhow!("send_data requires non-empty 'data'"));
556 }
557 let bytes = data.as_bytes().to_vec();
558 let dropped =
559 self.with_serial_handler(&action, |serial| serial.queue_tx(&bytes))?;
560 if dropped > 0 {
561 return Err(anyhow::anyhow!(
565 "send_data queued {} of {} byte(s); the transmit buffer is full because \
566 the host is not reading. Wait for it to drain before sending more.",
567 bytes.len() - dropped,
568 bytes.len()
569 ));
570 }
571 Ok(ActionResult::NoAction)
572 }
573 "set_line_coding" => {
574 let baud_raw = action["baud_rate"]
579 .as_u64()
580 .context("set_line_coding requires 'baud_rate'")?;
581 let baud_rate = u32::try_from(baud_raw).ok().filter(|b| *b > 0).context(
582 "set_line_coding 'baud_rate' must be between 1 and 4294967295 (CDC PSTN 1.2 \
583 encodes dwDTERate as 32 bits)",
584 )?;
585
586 let data_bits_raw = action["data_bits"].as_u64().unwrap_or(8);
587 if !matches!(data_bits_raw, 5 | 6 | 7 | 8 | 16) {
588 return Err(anyhow::anyhow!(
589 "set_line_coding 'data_bits' is {}; CDC PSTN 1.2 table 17 allows only \
590 5, 6, 7, 8 or 16",
591 data_bits_raw
592 ));
593 }
594 let data_bits = data_bits_raw as u8;
595
596 let parity = match action["parity"].as_str().unwrap_or("none") {
599 "none" => 0,
600 "odd" => 1,
601 "even" => 2,
602 "mark" => 3,
603 "space" => 4,
604 other => {
605 return Err(anyhow::anyhow!(
606 "set_line_coding: unknown parity '{}' (expected none, odd, even, \
607 mark or space)",
608 other
609 ))
610 }
611 };
612 let stop_bits = match action["stop_bits"].as_f64().unwrap_or(1.0) {
613 v if (v - 1.0).abs() < f64::EPSILON => 0,
614 v if (v - 1.5).abs() < f64::EPSILON => 1,
615 v if (v - 2.0).abs() < f64::EPSILON => 2,
616 other => {
617 return Err(anyhow::anyhow!(
618 "set_line_coding: unknown stop_bits {} (expected 1, 1.5 or 2)",
619 other
620 ))
621 }
622 };
623
624 let line_coding = crate::server::usb::descriptors::LineCoding {
625 baud_rate,
626 stop_bits,
627 parity,
628 data_bits,
629 };
630 self.with_serial_handler(&action, |serial| serial.set_line_coding(line_coding))?;
631 Ok(ActionResult::NoAction)
632 }
633 "wait_for_more" => Ok(ActionResult::WaitForMore),
634 _ => Err(anyhow::anyhow!("Unknown action type: {}", action_type)),
635 }
636 }
637}