1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub const MAX_TRANSFER_BYTES: usize = 64 * 1024;
22
23fn byte_field(action: &serde_json::Value, field: &str, action_type: &str) -> Result<u8> {
29 let value = action
30 .get(field)
31 .and_then(|v| v.as_u64())
32 .with_context(|| format!("Missing '{field}' field"))?;
33 u8::try_from(value).map_err(|_| {
34 anyhow::anyhow!("{action_type} '{field}' is {value}, outside the 0..=255 a USB byte holds")
35 })
36}
37
38fn word_field(action: &serde_json::Value, field: &str, action_type: &str) -> Result<u16> {
40 let value = action
41 .get(field)
42 .and_then(|v| v.as_u64())
43 .with_context(|| format!("Missing '{field}' field"))?;
44 u16::try_from(value).map_err(|_| {
45 anyhow::anyhow!(
46 "{action_type} '{field}' is {value}, outside the 0..=65535 a USB 16-bit field holds"
47 )
48 })
49}
50
51fn transfer_length(action: &serde_json::Value, action_type: &str, default: u64) -> Result<usize> {
55 let value = action
56 .get("length")
57 .and_then(|v| v.as_u64())
58 .unwrap_or(default);
59 if value as u128 > MAX_TRANSFER_BYTES as u128 {
60 return Err(anyhow::anyhow!(
61 "{action_type} 'length' is {value}; at most {MAX_TRANSFER_BYTES} bytes may be \
62 requested in one transfer"
63 ));
64 }
65 Ok(value as usize)
66}
67
68pub static USB_DEVICE_OPENED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
70 EventType::new(
71 "usb_device_opened",
72 "USB device successfully opened and interface claimed",
73 json!({
74 "type": "control_transfer",
75 "request_type": 0x80,
76 "request": 0x06,
77 "value": 0x0100,
78 "index": 0,
79 "length": 18
80 }),
81 )
82 .with_parameters(vec![
83 Parameter {
84 name: "vendor_id".to_string(),
85 type_hint: "string".to_string(),
86 description: "USB vendor ID (hex)".to_string(),
87 required: true,
88 },
89 Parameter {
90 name: "product_id".to_string(),
91 type_hint: "string".to_string(),
92 description: "USB product ID (hex)".to_string(),
93 required: true,
94 },
95 Parameter {
96 name: "manufacturer".to_string(),
97 type_hint: "string".to_string(),
98 description: "Device manufacturer string".to_string(),
99 required: false,
100 },
101 Parameter {
102 name: "product".to_string(),
103 type_hint: "string".to_string(),
104 description: "Device product string".to_string(),
105 required: false,
106 },
107 ])
108});
109
110pub static USB_CONTROL_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
112 EventType::new(
113 "usb_control_response",
114 "Response received from USB control transfer",
115 json!({
116 "type": "wait_for_more"
117 }),
118 )
119 .with_parameters(vec![
120 Parameter {
121 name: "data_hex".to_string(),
122 type_hint: "string".to_string(),
123 description: "Response data (as hex string)".to_string(),
124 required: true,
125 },
126 Parameter {
127 name: "data_length".to_string(),
128 type_hint: "number".to_string(),
129 description: "Length of response in bytes".to_string(),
130 required: true,
131 },
132 ])
133});
134
135pub static USB_BULK_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
137 EventType::new(
138 "usb_bulk_data_received",
139 "Data received from USB bulk endpoint",
140 json!({
141 "type": "bulk_transfer_out",
142 "endpoint": 0x02,
143 "data_hex": "48656c6c6f"
144 }),
145 )
146 .with_parameters(vec![
147 Parameter {
148 name: "data_hex".to_string(),
149 type_hint: "string".to_string(),
150 description: "The data received (as hex string)".to_string(),
151 required: true,
152 },
153 Parameter {
154 name: "data_length".to_string(),
155 type_hint: "number".to_string(),
156 description: "Length of data in bytes".to_string(),
157 required: true,
158 },
159 Parameter {
160 name: "endpoint".to_string(),
161 type_hint: "number".to_string(),
162 description: "Endpoint address".to_string(),
163 required: true,
164 },
165 ])
166});
167
168pub static USB_INTERRUPT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
170 EventType::new(
171 "usb_interrupt_data_received",
172 "Data received from USB interrupt endpoint",
173 json!({
174 "type": "wait_for_more"
175 }),
176 )
177 .with_parameters(vec![
178 Parameter {
179 name: "data_hex".to_string(),
180 type_hint: "string".to_string(),
181 description: "The data received (as hex string)".to_string(),
182 required: true,
183 },
184 Parameter {
185 name: "data_length".to_string(),
186 type_hint: "number".to_string(),
187 description: "Length of data in bytes".to_string(),
188 required: true,
189 },
190 Parameter {
191 name: "endpoint".to_string(),
192 type_hint: "number".to_string(),
193 description: "Endpoint address".to_string(),
194 required: true,
195 },
196 ])
197});
198
199pub struct UsbClientProtocol;
201
202impl UsbClientProtocol {
203 pub fn new() -> Self {
204 Self
205 }
206}
207
208impl Protocol for UsbClientProtocol {
210 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
211 vec![ActionDefinition {
216 name: "detach_device".to_string(),
217 description: "Detach from the USB device and close connection".to_string(),
218 parameters: vec![],
219 example: json!({
220 "type": "detach_device"
221 }),
222 log_template: None,
223 }]
224 }
225
226 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
227 vec![
228 ActionDefinition {
229 name: "control_transfer".to_string(),
230 description: "Send a USB control transfer request".to_string(),
231 parameters: vec![
232 Parameter {
233 name: "request_type".to_string(),
234 type_hint: "number".to_string(),
235 description: "Request type byte (bmRequestType)".to_string(),
236 required: true,
237 },
238 Parameter {
239 name: "request".to_string(),
240 type_hint: "number".to_string(),
241 description: "Request byte (bRequest)".to_string(),
242 required: true,
243 },
244 Parameter {
245 name: "value".to_string(),
246 type_hint: "number".to_string(),
247 description: "Value word (wValue)".to_string(),
248 required: true,
249 },
250 Parameter {
251 name: "index".to_string(),
252 type_hint: "number".to_string(),
253 description: "Index word (wIndex)".to_string(),
254 required: true,
255 },
256 Parameter {
257 name: "data_hex".to_string(),
258 type_hint: "string".to_string(),
259 description: "Data to send (hex encoded, empty for IN transfers)"
260 .to_string(),
261 required: false,
262 },
263 Parameter {
264 name: "length".to_string(),
265 type_hint: "number".to_string(),
266 description: "Expected response length (for IN transfers)".to_string(),
267 required: false,
268 },
269 ],
270 example: json!({
271 "type": "control_transfer",
272 "request_type": 0x80,
273 "request": 0x06,
274 "value": 0x0100,
275 "index": 0,
276 "length": 18
277 }),
278 log_template: None,
279 },
280 ActionDefinition {
281 name: "bulk_transfer_out".to_string(),
282 description: "Send data via USB bulk OUT endpoint".to_string(),
283 parameters: vec![
284 Parameter {
285 name: "endpoint".to_string(),
286 type_hint: "number".to_string(),
287 description: "Bulk OUT endpoint address".to_string(),
288 required: true,
289 },
290 Parameter {
291 name: "data_hex".to_string(),
292 type_hint: "string".to_string(),
293 description: "Data to send (hex encoded)".to_string(),
294 required: true,
295 },
296 ],
297 example: json!({
298 "type": "bulk_transfer_out",
299 "endpoint": 0x02,
300 "data_hex": "48656c6c6f"
301 }),
302 log_template: None,
303 },
304 ActionDefinition {
305 name: "bulk_transfer_in".to_string(),
306 description: "Read data from USB bulk IN endpoint".to_string(),
307 parameters: vec![
308 Parameter {
309 name: "endpoint".to_string(),
310 type_hint: "number".to_string(),
311 description: "Bulk IN endpoint address".to_string(),
312 required: true,
313 },
314 Parameter {
315 name: "length".to_string(),
316 type_hint: "number".to_string(),
317 description: "Number of bytes to read".to_string(),
318 required: true,
319 },
320 ],
321 example: json!({
322 "type": "bulk_transfer_in",
323 "endpoint": 0x81,
324 "length": 64
325 }),
326 log_template: None,
327 },
328 ActionDefinition {
329 name: "interrupt_transfer_in".to_string(),
330 description: "Read data from USB interrupt IN endpoint".to_string(),
331 parameters: vec![
332 Parameter {
333 name: "endpoint".to_string(),
334 type_hint: "number".to_string(),
335 description: "Interrupt IN endpoint address".to_string(),
336 required: true,
337 },
338 Parameter {
339 name: "length".to_string(),
340 type_hint: "number".to_string(),
341 description: "Number of bytes to read".to_string(),
342 required: true,
343 },
344 ],
345 example: json!({
346 "type": "interrupt_transfer_in",
347 "endpoint": 0x83,
348 "length": 8
349 }),
350 log_template: None,
351 },
352 ActionDefinition {
353 name: "claim_interface".to_string(),
354 description: "Claim a USB interface for exclusive access".to_string(),
355 parameters: vec![Parameter {
356 name: "interface_number".to_string(),
357 type_hint: "number".to_string(),
358 description: "Interface number to claim".to_string(),
359 required: true,
360 }],
361 example: json!({
362 "type": "claim_interface",
363 "interface_number": 0
364 }),
365 log_template: None,
366 },
367 ActionDefinition {
368 name: "wait_for_more".to_string(),
369 description: "Wait for more data before responding".to_string(),
370 parameters: vec![],
371 example: json!({
372 "type": "wait_for_more"
373 }),
374 log_template: None,
375 },
376 ]
377 }
378
379 fn protocol_name(&self) -> &'static str {
380 "USB"
381 }
382
383 fn get_event_types(&self) -> Vec<EventType> {
384 vec![
385 EventType::new(
386 "usb_device_opened",
387 "Triggered when USB device is opened and interface claimed",
388 json!({"type": "placeholder", "event_id": "usb_device_opened"}),
389 ),
390 EventType::new(
391 "usb_control_response",
392 "Triggered when USB control transfer completes",
393 json!({"type": "placeholder", "event_id": "usb_control_response"}),
394 ),
395 EventType::new(
396 "usb_bulk_data_received",
397 "Triggered when data is received from bulk endpoint",
398 json!({"type": "placeholder", "event_id": "usb_bulk_data_received"}),
399 ),
400 EventType::new(
401 "usb_interrupt_data_received",
402 "Triggered when data is received from interrupt endpoint",
403 json!({"type": "placeholder", "event_id": "usb_interrupt_data_received"}),
404 ),
405 ]
406 }
407
408 fn stack_name(&self) -> &'static str {
409 "USB"
410 }
411
412 fn keywords(&self) -> Vec<&'static str> {
413 vec!["usb", "usb device", "usb client", "connect to usb"]
414 }
415
416 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
417 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
418
419 ProtocolMetadataV2::builder()
420 .state(DevelopmentState::Experimental)
421 .implementation("nusb pure-Rust USB library for device access")
422 .llm_control("Full control over USB transfers (control, bulk, interrupt)")
423 .e2e_testing("Requires actual USB device for testing")
424 .build()
425 }
426
427 fn description(&self) -> &'static str {
428 "USB client for low-level USB device interaction"
429 }
430
431 fn example_prompt(&self) -> &'static str {
432 "Connect to USB device with vendor ID 0x1234, product ID 0x5678"
433 }
434
435 fn group_name(&self) -> &'static str {
436 "Hardware"
437 }
438 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
439 use crate::llm::actions::StartupExamples;
440 use serde_json::json;
441
442 StartupExamples::new(
443 json!({
445 "type": "open_client",
446 "remote_addr": "usb:1234:5678",
447 "base_stack": "usb",
448 "instruction": "Connect to USB device and get device descriptor"
449 }),
450 json!({
452 "type": "open_client",
453 "remote_addr": "usb:1234:5678",
454 "base_stack": "usb",
455 "event_handlers": [{
456 "event_pattern": "usb_device_opened",
457 "handler": {
458 "type": "script",
459 "language": "python",
460 "code": "<usb_client_handler>"
461 }
462 }]
463 }),
464 json!({
466 "type": "open_client",
467 "remote_addr": "usb:1234:5678",
468 "base_stack": "usb",
469 "event_handlers": [{
470 "event_pattern": "usb_device_opened",
471 "handler": {
472 "type": "static",
473 "actions": [{
474 "type": "control_transfer",
475 "request_type": 128,
476 "request": 6,
477 "value": 256,
478 "index": 0,
479 "length": 18
480 }]
481 }
482 }]
483 }),
484 )
485 }
486}
487
488impl Client for UsbClientProtocol {
490 fn connect(
491 &self,
492 ctx: crate::protocol::ConnectContext,
493 ) -> std::pin::Pin<
494 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
495 > {
496 Box::pin(async move {
497 use crate::client::usb::UsbClient;
498 UsbClient::connect_with_llm_actions(
499 ctx.remote_addr,
500 ctx.llm_client,
501 ctx.state,
502 ctx.status_tx,
503 ctx.client_id,
504 )
505 .await
506 })
507 }
508
509 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
510 let action_type = action
511 .get("type")
512 .and_then(|v| v.as_str())
513 .context("Missing 'type' field in action")?;
514
515 match action_type {
516 "control_transfer" => {
517 let request_type = byte_field(&action, "request_type", action_type)?;
518 let request = byte_field(&action, "request", action_type)?;
519 let value = word_field(&action, "value", action_type)?;
520 let index = word_field(&action, "index", action_type)?;
521
522 let data_hex = action
523 .get("data_hex")
524 .and_then(|v| v.as_str())
525 .unwrap_or("");
526
527 let length = transfer_length(&action, action_type, 0)?;
528
529 let data = if !data_hex.is_empty() {
530 let bytes = hex::decode(data_hex).context("Invalid hex data")?;
531 if bytes.len() > MAX_TRANSFER_BYTES {
532 return Err(anyhow::anyhow!(
533 "control_transfer 'data_hex' decodes to {} bytes; at most {} are \
534 accepted",
535 bytes.len(),
536 MAX_TRANSFER_BYTES
537 ));
538 }
539 bytes
540 } else {
541 Vec::new()
542 };
543
544 Ok(ClientActionResult::Custom {
545 name: "control_transfer".to_string(),
546 data: json!({
547 "request_type": request_type,
548 "request": request,
549 "value": value,
550 "index": index,
551 "data": data,
552 "length": length,
553 }),
554 })
555 }
556 "bulk_transfer_out" => {
557 let endpoint = byte_field(&action, "endpoint", action_type)?;
558
559 let data_hex = action
560 .get("data_hex")
561 .and_then(|v| v.as_str())
562 .context("Missing 'data_hex' field")?;
563
564 let data = hex::decode(data_hex).context("Invalid hex data")?;
565 if data.len() > MAX_TRANSFER_BYTES {
566 return Err(anyhow::anyhow!(
567 "bulk_transfer_out 'data_hex' decodes to {} bytes; at most {} are \
568 accepted",
569 data.len(),
570 MAX_TRANSFER_BYTES
571 ));
572 }
573
574 Ok(ClientActionResult::Custom {
575 name: "bulk_transfer_out".to_string(),
576 data: json!({
577 "endpoint": endpoint,
578 "data": data,
579 }),
580 })
581 }
582 "bulk_transfer_in" => {
583 let endpoint = byte_field(&action, "endpoint", action_type)?;
584 let length = transfer_length(&action, action_type, 64)?;
585
586 Ok(ClientActionResult::Custom {
587 name: "bulk_transfer_in".to_string(),
588 data: json!({
589 "endpoint": endpoint,
590 "length": length,
591 }),
592 })
593 }
594 "interrupt_transfer_in" => {
595 let endpoint = byte_field(&action, "endpoint", action_type)?;
596 let length = transfer_length(&action, action_type, 64)?;
597
598 Ok(ClientActionResult::Custom {
599 name: "interrupt_transfer_in".to_string(),
600 data: json!({
601 "endpoint": endpoint,
602 "length": length,
603 }),
604 })
605 }
606 "claim_interface" => {
607 let interface_number = byte_field(&action, "interface_number", action_type)?;
608
609 Ok(ClientActionResult::Custom {
610 name: "claim_interface".to_string(),
611 data: json!({
612 "interface_number": interface_number,
613 }),
614 })
615 }
616 "detach_device" => Ok(ClientActionResult::Disconnect),
617 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
618 _ => Err(anyhow::anyhow!(
619 "Unknown USB client action: {}",
620 action_type
621 )),
622 }
623 }
624}